Second Life of a Hungarian SharePoint Geek

July 7, 2018

Checking user properties in Active Directory using PowerShell to identify logon issues

Filed under: Active Directory, PowerShell, Tips & Tricks — Tags: , , — Peter Holpar @ 17:45

While supporting a SharePoint environment having several thousands of users from multiple Active Directory domains, we have quite often complains, that one can not access the site. Beyond trivial network related problems, like incorrect proxy settings, it is probably the second most common reason for such complains having issues with the Active Directory account of the user.

To support such cases, I wrote a short PowerShell script that checks for the most common problems, like User must change password at next logon flag is activated, account is disabled or locked out, and password expired. Prerequisite: you should have PowerShell Active Directory module installed.

$userLogin = ‘Domain\UserName’
$userLoginSplitted = $userLogin.Split(‘\’)
$domainName = $userLoginSplitted[0]
$dcServer = Get-ADDomainController -Discover -DomainName $domainName

$user = Get-ADUser -Identity $userLoginSplitted[1] -Server $dcServer.HostName[0] -Properties Enabled, LockedOut, PwdLastSet, PasswordNeverExpires, msDS-UserPasswordExpiryTimeComputed
$pwdNeverExp = $user.PasswordNeverExpires
$pwdExpiresOn = If ($pwdNeverExp) { $null } Else { [DateTime]::FromFileTime($user."msDS-UserPasswordExpiryTimeComputed") }

Write-Host Checking user: $userLogin
Write-Host User must change password at next logon: $($user.PwdLastSet -eq 0)
Write-Host Account disabled: $(!$user.Enabled)
Write-Host Account locked out: $($user.LockedOut)
Write-Host Password expired: $((!$pwdNeverExp) -and ($pwdExpiresOn -lt [DateTime]::Now))

First thing to highlight in the script is how we get and use the domain controller. Getting the domain controller is the easy part, after splitting the user login name to a domain name and a user name, you simply invoke the Get-ADDomainController cmdlet with the Discover switch and passing the domain name you are looking for in the DomainName parameter.

Using the value returned is a bit more complicated, at least until you learn how to do it the right way. Although Active Directory cmdlets support the Server parameter, there is a lot of confusion how to use it correctly. If you simply pass the domain controller as you received it in the previous step from the Get-ADDomainController cmdlet, like:

$user = Get-ADUser -Identity $userLoginSplitted[1] -Server $dcServer

you receive this error message:

Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Web Services running.

After researching the samples on the web (like Get-ADUser -Server ‘servername’) and the official documentation of the Get-ADUser cmdlet we realized that the Server parameter requires a string value, so passing the $dcServer (of type Microsoft.ActiveDirectory.Management.ADDirectoryServer) was really not a good idea. But wait, scrolling through the properties of $dcServer by PowerShell autocomplete shows it has a property called HostName. That sounds really promising! Try it out!

$user = Get-ADUser -Identity $userLoginSplitted[1] -Server $dcServer.HostName

Now you have another error message:

Cannot convert ‘Microsoft.ActiveDirectory.Management.ADPropertyValueCollection’ to the type ‘System.String’ required by parameter ‘Server’. Specified method is not supported.

Well, that means that the HostName property is of type Microsoft.ActiveDirectory.Management.ADPropertyValueCollection and not a string either, even though PowerShell displays it as string if you output it like $dcServer.HostName. To get the name of the domain controller as string you should address it in the collection using an array indexer, like $dcServer.HostName[0].

So the right usage is:

$user = Get-ADUser -Identity $userLoginSplitted[1] -Server $dcServer.HostName[0]

although you might check first if there was any domain controller found ($dcServer and its HostName property not null, and HostName has at leas a single entry) if you want to be sure.

Note furthermore, that the largest possible 64-bit value (2^63-1 = 9223372036854775807) in property msDS-UserPasswordExpiryTimeComputed means PasswordNeverExpires is true. Invoking the FromFileTime method using this value would cause an error:

Exception calling "FromFileTime" with "1" argument(s): "Not a valid Win32 FileTime. Parameter name: fileTime"

Advertisements

Automating the Provisioning of a PWA-Instance in Project Server 2016

Filed under: ALM, PowerShell, PS 2016 — Tags: , , — Peter Holpar @ 17:43

Yet in 2015 I wrote a post about the automatic provisioning of a PWA instance. That time, it discussed the process in the context of Project Server 2013, now I updated the description to the version 2016.

Preparations

First of all, be sure you have enabled the usage of Project Server in the farm by entering a valid license key, otherwise you receive the error message below, when enabling the PWASITE feature at the end of the provisioning:

Enable-SPFeature : The farm does not have a product key for Project Server.
You can add your key by using Enable-ProjectServerLicense

As suggested by the error message, you should add your key by using Enable-ProjectServerLicense like:

Enable-ProjectServerLicense -Key [Guid of you Project Server product key]

At this step you might receive a further error message, stating:

Enable-ProjectServerLicense : We encountered an error while enabling Project
Server 2016 . Project Server 2016  requires the farm to have a valid
SharePoint Server Enterprise product key.

It means Project Server requires a licensed Enterprise version of SharePoint. In our case it was a bit confusing, as the page Upgrade and Migration / Enable Enterprise Features in Cental Administration displayed that the farm has already the Enterprise features activated.

image

The Upgrade and Migration / Convert License Type answered the question, as it turned out the farm was not licensed at all, being in state:

SharePoint Server Trial with Enterprise Client Access License

image

After entering the correct license key for the enterprise version to the  Enter the Product Key text field an submitting the form:

image

On the Convert License Type page the Current License changed to:

SharePoint Server Trial with Enterprise Client Access

and the Enter the Product Key text field was no more editable.

image

Unfortunately, I have not found any solution to convert the farm license type by PowerShell, so if you need it, you should perform it in Cental Administration user interface manually. The process of converting a license type is discussed in this post in details.

After we have a licensed Enterprise version, we can enable the Project Server license as well. Note, that you should restart your PowerShell console after converting the license type from Cental Administration as discussed above, otherwise it seems to be unable to recognize the change, and you receive the same error message about the lack of a valid SharePoint Server Enterprise product key as earlier.

Enable-ProjectServerLicense -Key [Guid of you Project Server product key]

The result should be now:

Project Server 2016  is now enabled.

Second part of the preparation is to ensure you have the adequate permissions to perform the provisioning job. Personally I prefer to have db_owner membership in all databases in the farm, including the configuration database as well. It is particularly important to check the permissions, if you plan to use an already existing content database to provision your PWA site, that you created via Central Administration site and not by PowerShell. Based on my experience the databases created in CA have different permissions configured as the ones created by PowerShell, and the lack of the permission may cause your provisioning process to stuck with no trivial solution.

Provisioning

After being prepared, let’s see our PowerShell script that provisions a new PWA instance, including:
– A separate SharePoint content database that should contain only a single site collection: the one for the PWA. If the content DB already exists, we will use the existing one, otherwise we create a new one.
– Creating the managed path for the PWA.
– A new site collection is created for the PWA using the project web application site template, and the right locale ID (1033 in our case). If the site collection already exists (in case we re-use a former content DB), it will be dropped before creating the new one.
– The PWASITE feature will be activated on the new site collection.
– Changing to the Project Server security model.
– Disabling quotas.

  1. # change this configuration values according the values in your farm
  2. $webAppUrl = 'http://YourSharePointServer'
  3. $contentDBName = 'ContentDB_PWA'
  4. $contentDBServer = 'YourSQLServer'
  5.  
  6. $pwaMgdPathPostFix = "PWA"
  7. $pwaUrl = [string]::Format("{0}/{1}", $webAppUrl, $pwaMgdPathPostFix)
  8. $pwaTitle = "PWA Site"
  9. $pwaSiteTemplate = "PWA#0"
  10. $pwaLcid = 1033 # English
  11. $ownerAlias = "domain\user1"
  12. $secondaryOwnerAlias = "domain\user2"
  13.  
  14. Write-Host Getting web application at $webAppUrl
  15. $webApp = Get-SPWebApplication -Identity $webAppUrl
  16.  
  17. # create the content database if needed
  18. $contentDatabase = Get-SPContentDatabase -Identity $contentDBName
  19. if ($contentDatabase -eq $null) {
  20.   Write-Host Creating content database: $contentDBName
  21.   $contentDatabase = New-SPContentDatabase -Name $contentDBName -WebApplication $webApp -MaxSiteCount 1 -WarningSiteCount 0 -DatabaseServer $contentDBServer
  22. }
  23. else {
  24.   Write-Host Using existing content database: $contentDBName
  25. }
  26.  
  27. # create the managed path if needed
  28. $pwaMgdPath = Get-SPManagedPath -Identity $pwaMgdPathPostFix -WebApplication $webApp -ErrorAction SilentlyContinue
  29. if ($pwaMgdPath -eq $null) {
  30.   Write-Host Creating managed path: $pwaMgdPathPostFix
  31.   $pwaMgdPath = New-SPManagedPath -RelativeURL $pwaMgdPathPostFix -WebApplication $webApp -Explicit
  32. }
  33. else {
  34.   Write-Host Using existing managed path: $pwaMgdPathPostFix
  35. }
  36. # (re)creating site collection for the PWA instance
  37. # we delete the site collection if it already exists
  38. $pwaSite = Get-SPSite -Identity $pwaUrl -ErrorAction SilentlyContinue
  39. if ($pwaSite -ne $null) {
  40.   Write-Host Deleting existing PWA site at $pwaUrl
  41.   $pwaSite.Delete()
  42. }
  43.  
  44. Write-Host Creating PWA site at $pwaUrl
  45.   $pwaSite = New-SPSite -Url $pwaUrl OwnerAlias $ownerAlias SecondaryOwnerAlias$secondaryOwnerAlias -ContentDatabase $contentDatabase Template $pwaSiteTemplate -Language $pwaLcid -Name $pwaTitle
  46.  
  47. # Enable PWASITE feature
  48. Enable-SPFeature pwasite -URL $pwaUrl
  49.  
  50. # Enable Project Server Permissions mode
  51. Set-SPProjectPermissionMode -Url $pwaUrl -Mode ProjectServer
  52.  
  53. # disabling qutoa
  54. $quota = Get-SPProjectDatabaseQuota -Url $pwaUrl
  55. $quota.IsEnabled = $false
  56. $quota.MaxDbMegaByteSize++
  57. Set-SPProjectDatabaseQuota -Url $pwaUrl $quota

A comment regarding the last step, disabling quota. If you simply read the values of the current quota using the Get-SPProjectDatabaseQuota cmdlet, disable the quota, and try to set the value by Set-SPProjectDatabaseQuota cmdlet, you get an error message:

Set-SPProjectDatabaseQuota : Cannot apply settings, the maximum database size must be greater than the read only limit.

That is because the properties MaxDbMegaByteSize and ReadOnlyMegaByteLimit have by default the same value (10240). Funny, that it later not allowed to set the same values yourself. That is why we have $quota.MaxDbMegaByteSize++ in code.

July 1, 2018

The sub-webs delivered to you are dirty

Filed under: PowerShell, SP 2013 — Tags: , — Peter Holpar @ 14:39

Recently I had to perform a simple administrative task: a SharePoint website had several sub-webs, each of them having unique permissions. Our goal was to reset the permissions to be inherited from the parent site. So I created a simple PowerShell script to achieve the goal:

$rootWeb = Get-SPWeb ‘http://YourSharePointSite/SomeSite’
$rootWeb.Webs | % { $_.ResetRoleInheritance() }

Note: Although I use the Webs property of the SPWeb object overall in this post to illustrate the problem and later the solution, the very same applies to the GetSubwebsForCurrentUser method of the SPWeb object as well.

To my greatest surprise, I received this error message for each of the sub-web sites:

Exception calling "ResetRoleInheritance" with "0" argument(s): "There are uncommitted changes on the SPWeb object, call SPWeb.Update() to commit the changes before calling this method."

To understand the source of the exception, I followed the call-chain of the SPWeb.ResetRoleInheritance method using Reflector. All of the methods and classes mentioned in the post are declared in the Microsoft.SharePoint assembly in the Microsoft.SharePoint namespace.

First, the SPWeb.ResetRoleInheritance() method invokes the virtual SPSecurableObject.ResetRoleInheritance(), that invokes the internal SPSecurableObjectImpl.ResetRoleInheritance() method, that finally calls the private SPSecurableObjectImpl.RevertRoleInheritance(bool copyRoleAssignments, bool clearSubScopes) method, where the exception get thrown.

The value of the $StackTrace variable in PowerShell confirmed the result of my research:

   at Microsoft.SharePoint.SPSecurableObjectImpl.RevertRoleInheritance(Boolean copyRoleAssignments, Boolean clearSubScopes)
   at Microsoft.SharePoint.SPWeb.ResetRoleInheritance()
   at CallSite.Target(Closure , CallSite , Object )

The SPSecurableObjectImpl.RevertRoleInheritance method contains this condition:

if ((this.m_objectType == SPObjectType.Web) && this.m_web.IsDirty)
{
    throw new InvalidOperationException(SPResource.GetString("SPWebHasUnCommittedChange", new object[0]));
}

It means, the internal IsDirty property of the SPWeb class is checked to see, if there is any uncommitted change in the SPWeb instance. It is common, that your SPWeb instance gets dirty after you change some of its properties, but in our case, we apparently have not change anything, we still get the complain about “dirtiness” our web.

Let’s see, how to check if our SPWeb instance is dirty or not, and find some kind of workaround.

Forget the PowerShell example above for a while, and switch to C#. In this case, the base version of the code looks like this:

  1. using (SPSite site = new SPSite("http://YourSharePointSite/SomeSite"))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         foreach (SPWeb subWeb in web.Webs) // or the same with webs.GetSubwebsForCurrentUser()
  6.         {
  7.             try
  8.             {
  9.                 subWeb.ResetRoleInheritance();
  10.             }
  11.             finally
  12.             {
  13.                 subWeb.Dispose();
  14.             }
  15.         }
  16.     }
  17. }

Although the IsDirty method of the SPWeb is declared as private, we can access it via Reflection, as we did it in the extension method below:

  1. static class Extensions
  2. {
  3.     public static bool IsDirty(this SPWeb web)
  4.     {
  5.         var result = false;
  6.  
  7.         var pi_isDirty = typeof(SPWeb).GetProperty("IsDirty", BindingFlags.NonPublic | BindingFlags.Instance);
  8.         result = (bool)pi_isDirty.GetValue(web);
  9.  
  10.         return result;
  11.     }
  12. }

Having our extension method, we can dump out easily, if  the root web site and its sub-webs are dirty or not.

  1. using (SPSite site = new SPSite("http://YourSharePointSite/SomeSite"))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         Console.WriteLine("Web '{0}' is dirty: '{1}'", web.Url, web.IsDirty());
  6.         foreach (SPWeb subWeb in web.Webs) // or the same with webs.GetSubwebsForCurrentUser()
  7.         {
  8.             try
  9.             {
  10.                 Console.WriteLine("Web '{0}' is dirty: '{1}'", subWeb.Url, subWeb.IsDirty());
  11.                 subWeb.ResetRoleInheritance();
  12.             }
  13.             finally
  14.             {
  15.                 subWeb.Dispose();
  16.             }
  17.         }
  18.     }
  19. }

The result shows, that the parent site is not dirty, but all of it sub-webs (returned either by the Webs property or the GetSubwebsForCurrentUser method) are all dirty.

There are two possible workarounds for the issue. We should either call the Update method of the SPWeb instance before invoking the ResetRoleInheritance method, thus clearing the IsDirty flag, or if we don’t want to commit any possible changes, we can create another, clear SPWeb instance from scratch based on the ID or the Url of the original SPWeb object, and invoke the ResetRoleInheritance method on the new instance. The code sample below illustrates both of these options:

  1. using (SPSite site = new SPSite("http://YourSharePointSite/SomeSite"))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         Console.WriteLine("Web '{0}' is dirty: '{1}'", web.Url, web.IsDirty());
  6.  
  7.         foreach (SPWeb subWeb in web.Webs) // or the same with webs.GetSubwebsForCurrentUser()
  8.         {
  9.             try
  10.             {
  11.                 Console.WriteLine("Web '{0}' is dirty: '{1}'", subWeb.Url, subWeb.IsDirty());
  12.                 // option 1
  13.                 subWeb.Update();
  14.                 Console.WriteLine("Web '{0}' is dirty: '{1}'", subWeb.Url, subWeb.IsDirty());
  15.                 subWeb.ResetRoleInheritance();
  16.                 //// option 2
  17.                 //using (SPWeb subWebNew = site.OpenWeb(subWeb.ID)) // or site.OpenWeb(subWeb.Url)
  18.                 //{
  19.                 //    Console.WriteLine("Web '{0}' is dirty: '{1}'", subWebNew.Url, subWebNew.IsDirty());
  20.                 //    subWebNew.ResetRoleInheritance();
  21.                 //}
  22.             }
  23.             finally
  24.             {
  25.                 subWeb.Dispose();
  26.             }
  27.         }
  28.     }
  29. }

After this detour into C#, let’s go back to our original PowerShell sample. Although there are no extension methods in PowerShell, we can define a helper function to query and display the value of the IsDirty property, and we can apply both of the above workarounds to “clear” or web instance as well:

  1. function IsDirty($web) {
  2.     $pi = [Microsoft.SharePoint.SPWeb].GetProperty("IsDirty", [Reflection.BindingFlags] "NonPublic,Instance")
  3.     $isDirty = $pi.GetValue($web)
  4.     return $isDirty
  5.     Write-Host Web $($web.Url) is dirty $isDirty
  6. }
  7.  
  8. $rootWeb = Get-SPWeb 'http://YourSharePointSite/SomeSite'
  9. Write-Host Web $($rootWeb.Url) is dirty $($rootWeb.IsDirty)
  10. $rootWeb.Webs | % {
  11.     $web = $_
  12.     IsDirty $web
  13.     # option 1
  14.     $web.Update()
  15.     IsDirty $web
  16.     $web.ResetRoleInheritance()
  17.     ## option 2
  18.     #$webClear = Get-SPWeb $web.Url
  19.     #IsDirty $webClear
  20.     #$webClear.ResetRoleInheritance()
  21. }

But wait! Is there really nothing in PowerShell like extension methods in C#? Couldn’t we extend or object somehow to be able to write nicer code? About this theme and much more plan I write in a later post.

 

April 30, 2018

Creating statistics about web part usage from the SharePoint content database

Filed under: PowerShell, Reflection, SP 2013, SQL, Web part — Tags: , , , — Peter Holpar @ 21:08

Recently I had to create some statistics about SharePoint web site customizations, like on which pages are there Script Editor Web Parts, or Content Editor Web Parts, etc. I knew I could and probably should have done it by iterating through all web sites, all pages and then looking up the web parts on each page using SPLimitedWebPartManager class, but I was aware, the same information should be available via the content database as well, making it possible to query the info much easier and faster, although unsupported. In this post I describe, how you can do it, but use the solution at your own risk.

The web part information is stored in the AllWebParts table, the information about the pages in the AllDocs table. I joined these tables together for the first report about the Script Editor Web Parts.

SELECT AD.DirName + ‘/’ + AD.LeafName as PageUrl, AWP.tp_ZoneID as ZoneId, AWP.tp_PartOrder as WebPartOrder, AWP.tp_Class AS WebPartClass
FROM
AllWebParts AWP (nolock)
INNER JOIN AllDocs (nolock) AD ON AWP.tp_SiteId = AD.SiteId AND AWP.tp_PageUrlID = AD.Id
WHERE tp_Class LIKE ‘%ScriptEditorWebPart’

Next, I was to create a report about the Content Editor Web Parts, using a filter like:

WHERE tp_Class = ‘%ContentEditorWebPart’

However, no result found, although I was pretty sure, there are a lot of them in our web site. How is it possible?

To test it further, I’ve included a Script Editor Web Part and a Content Editor Web Part on the AllItems.aspx page of the Tasks list in one of our sub-site, and created a new query with the filter below:

WHERE DirName LIKE ‘%site/subsite/Lists/Tasks%’
AND LeafName LIKE ‘%AllItem%’

This was the result:

image

As you see, the Script Editor Web Part is there, and you see two further web parts (they should be the Content Editor Web Part and the XsltListViewWeb part, that was originally on the page and is responsible to display the task items in the list), however both of them with a NULL value in the WebPartClass column. What should it mean?

I have studied the structure of the AllWebParts table and the relations of its fields further, and found that there are two fields (tp_Class and tp_Assembly) that are always populated for the records, where the WebPartClass is not NULL, and there is a field called tp_WebPartTypeId – populated for each entries, even for those, where the WebPartClass , tp_Class and tp_Assembly fields are empty – that we could eventually use to find the matching web parts. But how? I made a search for ‘WebPartTypeId’ using .NET Reflector, and found the internal class Microsoft.SharePoint.WebPartPages.WebPartTypeInfo, having a private static method called GetWellKnownTypeIdDictionary that returns a Dictionary<Guid, Type> mapping Guids (WebPartTypeIds) to the actual web part type. Remark: The Guids in the WebPartTypeId are actually created from the MD5 hash of the bytes of the joined full assembly name and web part class name, see the internal static  GetTypeIdUnsafe(MD5HashProvider md5Provider, string typeFullName, string assemblyName) method of the internal sealed class Microsoft.SharePoint.ApplicationRuntime.SafeControls.

image

To support those so called well-known types in my former SQL-query, I wrote a short PowerShell script that invokes the private static GetWellKnownTypeIdDictionary method of the internal WebPartTypeInfo class, and emits the resulting Dictionary to a text file I can use to extend my query:

  1. $webPartTypeInfoType = [System.Type]::GetType('Microsoft.SharePoint.WebPartPages.WebPartTypeInfo, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c')
  2. $mi_GetWellKnownTypeIdDictionary = $webPartTypeInfoType.GetMethod('GetWellKnownTypeIdDictionary', [Reflection.BindingFlags]'NonPublic, Static')
  3. $wellKnownTypeIdDictionary = $mi_GetWellKnownTypeIdDictionary.Invoke($null, $null)
  4.  
  5. $wpTypes = $wellKnownTypeIdDictionary.Keys | % { "INSERT INTO @WPTypes VALUES ('$_', '$($wellKnownTypeIdDictionary[$_].Assembly.FullName)', '$($wellKnownTypeIdDictionary[$_].FullName)')" }
  6. Set-Content -Path 'C:\Data\WPTypes.txt' -Value $wpTypes

And that is already the extended version of the SQL query:

  1. DECLARE @WPTypes TABLE
  2.    (
  3.      Id uniqueidentifier NOT NULL,
  4.      AssemblyName varchar(500),
  5.      ClassName varchar(100)
  6.    )
  7.  
  8. INSERT INTO @WPTypes VALUES ('8e20cf70-0fd5-1e08-9972-38f63a6bd59a', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ImageWebPart')
  9. INSERT INTO @WPTypes VALUES ('ba009853-eac3-16c8-9094-a8834485ad33', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.DataFormWebPart')
  10. INSERT INTO @WPTypes VALUES ('83216ab2-cd0e-e9fc-fc5e-6a8f3b21c37b', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.DataViewWebPart')
  11. INSERT INTO @WPTypes VALUES ('42fddde2-e0cf-c8ab-48b7-db1fcac0a917', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ListFormWebPart')
  12. INSERT INTO @WPTypes VALUES ('05d0fd94-372a-5ee7-b480-ccb8f9cd2c23', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ListViewWebPart')
  13. INSERT INTO @WPTypes VALUES ('aef28218-44f8-0538-9805-4842c0e62811', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.XsltListFormWebPart')
  14. INSERT INTO @WPTypes VALUES ('a6524906-3fd2-ee4e-23ee-252d3c6e0dc9', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.XsltListViewWebPart')
  15. INSERT INTO @WPTypes VALUES ('0c6143a7-d68b-bade-e0ef-2c4d01182b0c', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.BlogAdminWebPart')
  16. INSERT INTO @WPTypes VALUES ('afef48e1-8f94-eb71-03a6-ffceb685306a', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.BlogMonthQuickLaunch')
  17. INSERT INTO @WPTypes VALUES ('4c06cea2-364f-47e3-e1d7-08d53f441157', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ContentEditorWebPart')
  18. INSERT INTO @WPTypes VALUES ('e6047383-438e-ed87-1a93-f1ff71729044', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.TitleBarWebPart')
  19. INSERT INTO @WPTypes VALUES ('707c1e73-0b3d-898b-c755-01621802ab8c', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.SilverlightWebPart')
  20. INSERT INTO @WPTypes VALUES ('28c23aec-2537-68b3-43b6-845b13cea19f', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ErrorWebPart')
  21. INSERT INTO @WPTypes VALUES ('8d6034c4-a416-e535-281a-6b714894e1aa', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ErrorWebPart')
  22. INSERT INTO @WPTypes VALUES ('8e814083-396a-e7d1-148b-316e3a7283f7', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ErrorWebPart')
  23. INSERT INTO @WPTypes VALUES ('e6377261-6920-bbfe-501f-fda7a61db10f', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ErrorWebPart')
  24. INSERT INTO @WPTypes VALUES ('8efd140d-eae9-5feb-06e3-f771842d2e43', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.ErrorWebPart')
  25. INSERT INTO @WPTypes VALUES ('b3294a07-46bf-e661-d036-10670590bbd3', 'Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', 'Microsoft.SharePoint.WebPartPages.SPUserCodeWebPart')
  26.  
  27. SELECT AD.DirName + '/' + AD.LeafName as PageUrl, AWP.tp_ZoneID as ZoneId, AWP.tp_PartOrder as WebPartOrder, ISNULL(AWP.tp_Class, WPT.ClassName) AS WebPartClass
  28. FROM AllWebParts AWP (nolock)
  29. INNER JOIN AllDocs AD (nolock) ON AWP.tp_SiteId = AD.SiteId AND AWP.tp_PageUrlID = AD.Id
  30. LEFT JOIN @WPTypes WPT ON AWP.tp_WebPartTypeId = WPT.Id
  31. WHERE ISNULL(AWP.tp_Class, WPT.ClassName) LIKE '%ContentEditorWebPart'

Of course, you can change the conditions of the query as you like, for example, you can restrict it to two web part type, like:

WHERE ISNULL(AWP.tp_Class, WPT.ClassName) IN (‘Microsoft.SharePoint.WebPartPages.ScriptEditorWebPart’, ‘Microsoft.SharePoint.WebPartPages.ContentEditorWebPart’)

There are a few more columns in the AllWebParts table, that you eventually would include either in the SELECT statement or in its WHERE clause, these are:

  • tp_IsIncluded: The web part is displayed on the page, if the value is 1 (default). If you close (not delete!) a web part, the value is 0. Deleted web parts are removed from the table.
  • tp_Deleted: Assume you have a list with some pages that includes web parts, like view pages including XsltListViewWebPart instances. The web part entries in the AllWebParts table have a value of 0 at this stage. If you delete the list, these values change to 1. The web part entries will be kept in the table even after deleting the list from the first (user) level Recycle Bin, and removed only after the list is deleted from the second (site collection) level Recycle Bin.
  • tp_ListId: This is a field that is populated for list-related built-in web parts, like XsltListViewWebPart. You can look up the related list and web instances by joining the Lists and Webs views in your query respectively, as shown below (this time I omit the declaration of the @WPTypes variable and its population with value for the sake of brevity, but of course, you need it this time either):

SELECT L.tp_Title as ListTitle, W.FullUrl AS WebUrl, AD.DirName + ‘/’ + AD.LeafName as PageUrl, AWP.tp_ZoneID as ZoneId, AWP.tp_PartOrder as WebPartOrder, ISNULL(AWP.tp_Class, WPT.ClassName) AS WebPartClass, tp_ListId
FROM AllWebParts AWP (nolock)
INNER JOIN AllDocs AD (nolock) ON AWP.tp_SiteId = AD.SiteId AND AWP.tp_PageUrlID = AD.Id
LEFT JOIN @WPTypes WPT ON AWP.tp_WebPartTypeId = WPT.Id
LEFT JOIN Lists L (nolock) ON AWP.tp_SiteId = L.tp_SiteId AND AWP.tp_ListId = L.tp_ID
LEFT JOIN Webs W (nolock) ON AWP.tp_SiteId = W.SiteId AND L.tp_WebId = W.Id
WHERE ISNULL(AWP.tp_Class, WPT.ClassName) LIKE ‘%ListViewWebPart’

By including the list title or the web URL in the WHERE clause (or the ID of the list or the web if you wish) you can further limit the items returned by the query.

If there are records returned with NULL in the ListTitle and WebUrl columns it means typically that the list was deleted, but yet available in the Recycle Bin. See my comments regarding the tp_Deleted field above. Note, that despite the name of the FullUrl column in the Web view, it is actually a server relative URL.

I hope this overview has helped you to better understand what and how is stored in these tables of the SharePoint content database, as well, how the “magical” IDs of the well-known web part types do fit into the whole picture.

March 23, 2018

Changing SharePoint Search Preferences from Code

Filed under: PowerShell, Reflection, Search, SP 2013 — Tags: , , , — Peter Holpar @ 22:22

In my recent post I’ve illustrated with C# and PowerShell examples, how to read search preferences info from code, both for the current user as well as for other users. In this post we will see, how to change that preference from code.

We will use the same object, the Microsoft.Office.Server.Search.Administration.UserPreference class. Unlike its static GetUserPreference method, the either static SetUserPreference method has no overload that accepts the SharePoint context (SPContext) as parameter. The single overload of this method accepts a UserPreference instance. That makes our life not easier when it comes later to changing the preferences for another user. Don’t lose the hope, it is not impossible. Bur first things first.

Once we received a UserPreference instance via the GetUserPreference method, you should change certain preference properties, like OpenDocumentsInClient. How to do it? There are two methods, EnableSettings and DisableSetting (both having a parameter of  the nested enumeration type Settings) defined in the UserPreference class. If you would like to activate a setting, you should call the EnableSettings method, if you need to deactivate it, call the DisableSetting method, then finally invoke the SetUserPreference method to persist the changes. For example:

  1. var userPref = UserPreference.GetUserPreference();
  2. // if you would like to open documents in Office client, like Word or Excel
  3. userPref.UpdateSetting(UserPreference.Settings.OpenDocumentsInClient, true);
  4. // if you would like to open documents in Browser (Office Web Apps)
  5. userPref.UpdateSetting(UserPreference.Settings.OpenDocumentsInClient, false);
  6. UserPreference.SetUserPreference(userPref);

Note: This code works only if  you try it within a SharePoint context, like on an application page or in web part. In a console application you will receive an exception when you invoke the overload of the GetUserPreference method without the SPContext parameter:

ArgumentNullException
The value must not be null.
Parameter name: SPContext.Current

As we’ve already seen, there is an overload of the GetUserPreference method that accepts a SPContext parameter, so you could use that to get the preferences, but as there is no such overload for the SetUserPreference method, at least at this points will be the same type of exception thrown again. We will revisit the question shortly, how to set your own preferences from a console application, but we make a quick detour first.

To tell the truth, I don’t like the above pattern at all. Instead of these two methods I created an extension method with a Boolean parameter that encapsulates the functionality:

  1. public static void UpdateSetting(this UserPreference userPreference, UserPreference.Settings setting, bool value)
  2. {
  3.     if (value)
  4.     {
  5.         userPreference.EnableSetting(setting);
  6.     }
  7.     else
  8.     {
  9.         userPreference.DisableSetting(setting);
  10.     }
  11. }

Using this new method one can enable / disable preference settings like:

  1. var userPref = UserPreference.GetUserPreference();
  2. // if you would like to open documents in Office client, like Word or Excel
  3. userPref.EnableSetting(UserPreference.Settings.OpenDocumentsInClient);
  4. // if you would like to open documents in Browser (Office Web Apps)
  5. userPref.DisableSetting(UserPreference.Settings.OpenDocumentsInClient);
  6. UserPreference.SetUserPreference(userPref);

Back to the question, how to set your own preferences when the code runs without SharePoint context, like from a console application?

The “trivial” way is to fake a SharePoint context, using the method described here:

  1. using (SPSite site = new SPSite(url))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         HttpRequest request = new HttpRequest(string.Empty, url, string.Empty);
  6.  
  7.         HttpResponse response = new HttpResponse(new System.IO.StreamWriter(new System.IO.MemoryStream()));
  8.  
  9.         HttpContext ctx = new HttpContext(request, response);
  10.         ctx.Items["HttpHandlerSPWeb"] = web;
  11.         HttpContext.Current = ctx;
  12.  
  13.         Console.WriteLine(SPContext.Current.Web.CurrentUser.LoginName);
  14.  
  15.         var userPref = UserPreference.GetUserPreference();
  16.         userPref.UpdateSetting(UserPreference.Settings.OpenDocumentsInClient, true);
  17.         UserPreference.SetUserPreference(userPref);
  18.  
  19.         //set back the original context (e.g. null)
  20.         HttpContext.Current = null;
  21.     }
  22. }

Another option is, to try to understand, how the SetUserPreference method internally works. It turns out, that it call the internal static UpdatePreference method:

UpdatePreference(preference, false, SPContext.Current);

So I’ve created just another extension method that wraps invoking the UpdatePreference method using Reflection:

  1. public static void Update(this UserPreference userPreference, bool fClearClickHistory, SPContext context)
  2. {
  3.     Type[] paramTypes = { typeof(UserPreference), typeof(bool), typeof(SPContext) };
  4.     MethodInfo updatePreference = userPreference.GetType().GetMethod("UpdatePreference", BindingFlags.Static | BindingFlags.NonPublic, null, paramTypes, null);
  5.     object[] parameters = { userPreference, fClearClickHistory, context };
  6.     updatePreference.Invoke(null, parameters);
  7. }

And a further helper method that accepts a SPContext object as parameter, writes out, preferences of which user we are to change and performs the change itself via the methods we have already:

  1. private void UpdatePreference(SPContext ctx)
  2. {
  3.     var user = ctx.Web.CurrentUser;
  4.     Console.WriteLine("Setting preferences for '{0}'", user.LoginName);
  5.  
  6.     var userPref = UserPreference.GetUserPreference(false, ctx);
  7.     userPref.UpdateSetting(UserPreference.Settings.OpenDocumentsInClient, false);
  8.     userPref.Update(false, ctx);
  9. }

I think the code we achieved using this extension method is much more readable as the former one with the dummy context:

  1. using (SPSite site = new SPSite(url))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         var ctx = SPContext.GetContext(web);
  6.         UpdatePreference(ctx);
  7.     }
  8. }

The code snippets until this point have effect only on the current user. How to change the settings for other user? That is possible either, as soon we combine the methods we already have with impersonation.

First, the version that uses the dummy context:

  1. using (SPSite site = new SPSite(url))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         var user = web.EnsureUser(@"i:0#.w|domain\user");
  6.  
  7.         SPSecurity.RunWithElevatedPrivileges(
  8.           () =>
  9.           {
  10.               using (SPSite impSite = new SPSite(url, user.UserToken))
  11.               using (SPWeb impWeb = impSite.OpenWeb())
  12.               {
  13.                   HttpRequest request = new HttpRequest(string.Empty, url, string.Empty);
  14.  
  15.                   HttpResponse response = new HttpResponse(new System.IO.StreamWriter(new System.IO.MemoryStream()));
  16.  
  17.                   HttpContext impersonatedContext = new HttpContext(request, response);
  18.  
  19.                   impersonatedContext.Items["HttpHandlerSPWeb"] = impWeb;
  20.  
  21.                   HttpContext.Current = impersonatedContext;
  22.  
  23.                   Console.WriteLine(SPContext.Current.Web.CurrentUser.LoginName);
  24.  
  25.                   var userPref = UserPreference.GetUserPreference();
  26.                   userPref.UpdateSetting(UserPreference.Settings.OpenDocumentsInClient, true);
  27.                   UserPreference.SetUserPreference(userPref);
  28.  
  29.                   //set back the original context (e.g. null)
  30.                   HttpContext.Current = null;
  31.               }
  32.           });
  33.     }
  34. }

Next, the other version using Reflection:

  1. using (SPSite site = new SPSite(url))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         var user = web.EnsureUser(@"i:0#.w|domain\user");
  6.  
  7.         SPSecurity.RunWithElevatedPrivileges(
  8.           () =>
  9.           {
  10.               using (SPSite impSite = new SPSite(url, user.UserToken))
  11.               using (SPWeb impWeb = impSite.OpenWeb())
  12.               {
  13.                   var impCtx = SPContext.GetContext(impWeb);
  14.                   UpdatePreference(impCtx);
  15.               }
  16.           });
  17.     }
  18. }

Mission completed.

For those of you who would like to have the same functionality from PowerShell (of course, there are no SharePoint context inherited from the process at all), I include the equivalent methods below.

These are the helper methods we rely on:

  1. function UpdateSetting($userPreference, $setting, $value) {
  2.     If ($value)
  3.     {
  4.         $userPreference.EnableSetting($setting)
  5.     }
  6.     Else
  7.     {
  8.         $userPreference.DisableSetting($setting)
  9.     }
  10. }
  11.  
  12. function Update($userPreference, $fClearClickHistory, $context) {
  13.     $paramTypes = ($up, [bool], [Microsoft.SharePoint.SPContext])
  14.     $updatePreference = $up.GetMethod("UpdatePreference", [System.Reflection.BindingFlags]"Static, NonPublic" , $null, $paramTypes, $null)
  15.     $parameters = ($userPreference, $fClearClickHistory, $context)
  16.     $updatePreference.Invoke($null, $parameters)
  17. }

Furthermore, we declared the following shortcuts:

  1. # shortcut for UserPreference
  2. $up = [Microsoft.Office.Server.Search.Administration.UserPreference]
  3. # shortcut for the nested class Settings in UserPreference
  4. $ups = [Microsoft.Office.Server.Search.Administration.UserPreference+Settings]

Set preferences for the current user via Reflection:

  1. $web = Get-SPWeb $url
  2. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($web)
  3.  
  4. $pref = $up::GetUserPreference($false, $ctx)
  5. UpdateSetting $pref $ups::OpenDocumentsInClient $true
  6. Update $pref $false $ctx

Set preferences for another user via Reflection:

  1. $userName = 'i:0#.w|domain\user'
  2.  
  3. $web = Get-SPWeb $url
  4. $user = $web.EnsureUser($userName)
  5. $userToken = $user.UserToken
  6.  
  7. $impersonatedSite = New-Object Microsoft.SharePoint.SPSite($url, $userToken)
  8. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($impersonatedSite.RootWeb)
  9.  
  10. $pref = $up::GetUserPreference($false, $ctx)
  11. UpdateSetting $pref $ups::OpenDocumentsInClient $true
  12. Update $pref $false $ctx

Set preferences for the current user using a dummy context (see this post about injecting a fake SharePoint context into PowerShell):

  1. $web = Get-SPWeb $url
  2. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($web)
  3.  
  4. $sw = New-Object System.IO.StringWriter
  5. $request = New-Object System.Web.HttpRequest "", $url, ""
  6. $response = New-Object System.Web.HttpResponse $sw
  7. $dummyContext = New-Object System.Web.HttpContext $request, $response
  8. [System.Web.HttpContext]::Current = $dummyContext
  9. $dummyContext.Items["HttpHandlerSPWeb"] = $ctx.Web;
  10.  
  11. $pref = $up::GetUserPreference($false, $ctx)
  12. #$pref.EnableSetting($ups::OpenDocumentsInClient)
  13. #or
  14. #$pref.DisableSetting($ups::OpenDocumentsInClient)
  15. UpdateSetting $pref $ups::OpenDocumentsInClient $true
  16. $up::SetUserPreference($pref)
  17.  
  18. [System.Web.HttpContext]::Current = $null

Set preferences for another user using a dummy context:

  1. $userName = 'i:0#.w|domain\user'
  2.  
  3. $web = Get-SPWeb $url
  4. $user = $web.EnsureUser($userName)
  5. $userToken = $user.UserToken
  6.  
  7. $impersonatedSite = New-Object Microsoft.SharePoint.SPSite($url, $userToken)
  8. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($impersonatedSite.RootWeb)
  9.  
  10. $sw = New-Object System.IO.StringWriter
  11. $request = New-Object System.Web.HttpRequest "", $url, ""
  12. $response = New-Object System.Web.HttpResponse $sw
  13. $dummyContext = New-Object System.Web.HttpContext $request, $response
  14. [System.Web.HttpContext]::Current = $dummyContext
  15. $dummyContext.Items["HttpHandlerSPWeb"] = $ctx.Web;
  16.  
  17. $pref = $up::GetUserPreference($false, $ctx)
  18. #$pref.EnableSetting($ups::OpenDocumentsInClient)
  19. #or
  20. #$pref.DisableSetting($ups::OpenDocumentsInClient)
  21. UpdateSetting $pref $ups::OpenDocumentsInClient $true
  22. $up::SetUserPreference($pref)
  23.  
  24. [System.Web.HttpContext]::Current = $null

Querying SharePoint Search Preferences from Code

Filed under: PowerShell, Search, SP 2013 — Tags: , , — Peter Holpar @ 22:20

Recently a user complained, that he is not able to open office documents from the SharePoint portal of the company when working at home. He attached a screenshot of the error message in the browser to his mail. From this screenshot was it obvious, that there is an issue with the accessibility of the Office Web Applications server (OWA, also known as WAC), see the word wac in the address on the browser screen or WopiFrame.aspx in the address bar.

image

As it turned out, the WAC-server of the company has not been published externally via the firewall, but it was on purpose. Users should have been able to work with documents using their locally installed Office applications.

As you might know, you can configure the behavior, if document would be opened in browser or in the Office client application instead on either the site collection or on the document library level (see details here). The documents the user complained about were located in a library with the setting “Use the server default (Open in the client application)”, but it has not helped, when we changed it to “Open in the client application” explicitly.

It was really curious, but after a little while it turned out, that he wanted to open the document not from the library, but from a search result. At least a step further to the solution, have we thought.

You should know, that the behavior, if the Office documents get opened in the client application or in the browser is independent from the site collection level settings as well as from the document library settings. There is a (in my personal opinion pretty hidden) Preference link at the bottom of the search results page:

image

On this page the users can configure their own preferences, among others, if they would like to open the Office documents in the client application or in the browser:

image

It’s a cool option to enable users to decide which way they prefer, although it is pretty inconsistent with the other options (available for the administrators) we mentioned earlier. But there is an even bigger issue (at least, for me) with that. There is (as far as I know) no option / UI for administrators to query the value configured for a user, not to mention, how to change it remotely, without end user interaction.

Although it might have been the easiest choice to ask the user, which value he has configured for himself , I’m not the man of easy options if there might be a programmatic approach as well and a chance to learn something new. So let’s see, what I’ve learned.

The user preferences regarding the search are available via the Microsoft.Office.Server.Search.Administration.UserPreference class. If you need the user preferences from the current SPContext (e.g. for the current user), you can use either the static GetUserPreference() method or the other static overload GetUserPreference(bool lookupFromCache). If, however, you need the preference for another user, you can inject it via the static GetUserPreference(bool lookupFromCache, SPContext context) method.

For example, the DisplayPreference method below displays a few of the available preferences from a context it receives as parameter:

  1. private void DisplayPreference(SPContext ctx)
  2. {
  3.     var user = ctx.Web.CurrentUser;
  4.     Console.WriteLine("Reading preferences for '{0}'", user.LoginName);
  5.  
  6.     var userPref = UserPreference.GetUserPreference(false, ctx);
  7.  
  8.     Console.WriteLine("ShowPrequerySuggestion: {0}", userPref.IsSettingEnabled(UserPreference.Settings.ShowPrequerySuggestion));
  9.     Console.WriteLine("ShowPersonalSuggestions: {0}", userPref.IsSettingEnabled(UserPreference.Settings.ShowPersonalSuggestions));
  10.     Console.WriteLine("OpenDocumentsInClient: {0}", userPref.IsSettingEnabled(UserPreference.Settings.OpenDocumentsInClient));
  11. }

The following code snippet (taken from a console application) invokes the DisplayPreference method first to display the preferences of the current user, then again to display the preferences of an impersonated user:

  1. using (SPSite site = new SPSite(url))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         var ctx = SPContext.GetContext(web);
  6.         DisplayPreference(ctx);
  7.  
  8.         var user = web.EnsureUser(@"i:0#.w|domain\user");
  9.  
  10.         SPSecurity.RunWithElevatedPrivileges(
  11.           () =>
  12.           {
  13.               using (SPSite impSite = new SPSite(url, user.UserToken))
  14.               using (SPWeb impWeb = impSite.OpenWeb())
  15.               {
  16.                   var impCtx = SPContext.GetContext(impWeb);
  17.  
  18.                   DisplayPreference(impCtx);
  19.               }
  20.           });
  21.     }
  22. }

Of course, if your code runs in a SharePoint process, you can get the context as SPContext.Current as well for the current user.

The same information is available via PowerShell either. For example, displaying preferences for the current user:

  1. $web = Get-SPWeb $url
  2. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($web)
  3.  
  4. # shortcut for UserPreference
  5. $up = [Microsoft.Office.Server.Search.Administration.UserPreference]
  6. # shortcut for the nested class Settings in UserPreference
  7. $ups = [Microsoft.Office.Server.Search.Administration.UserPreference+Settings]
  8.  
  9. $pref = $up::GetUserPreference($false, $ctx)
  10. $pref.IsSettingEnabled($ups::ShowPrequerySuggestion)
  11. $pref.IsSettingEnabled($ups::ShowPersonalSuggestions)
  12. $pref.IsSettingEnabled($ups::OpenDocumentsInClient)

If you need the preferences of another user, you should impersonate it first as described here. After the impersonation, the code is pretty the same as earlier:

  1. $userName = 'i:0#.w|domain\user'
  2.  
  3. $web = Get-SPWeb $url
  4. $user = $web.EnsureUser($userName)
  5. $userToken = $user.UserToken
  6.  
  7. $impersonatedSite = New-Object Microsoft.SharePoint.SPSite($url, $userToken)
  8. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($impersonatedSite.RootWeb)
  9.  
  10. # shortcut for UserPreference
  11. $up = [Microsoft.Office.Server.Search.Administration.UserPreference]
  12. # shortcut for the nested class Settings in UserPreference
  13. $ups = [Microsoft.Office.Server.Search.Administration.UserPreference+Settings]
  14.  
  15. $pref = $up::GetUserPreference($false, $ctx)
  16. $pref.IsSettingEnabled($ups::ShowPrequerySuggestion)
  17. $pref.IsSettingEnabled($ups::ShowPersonalSuggestions)
  18. $pref.IsSettingEnabled($ups::OpenDocumentsInClient)

Using this code we were able to detected that the complaining user has really the wrong preference (OpenDocumentsInClient was false). Now we had two choices: either to call the user, and ask him to change the preference, or to find a solution, how it would be possible to change it from code on behalf of the user remotely. Of course, this time we didn’t want to change the preferences without the explicit permission of the user, so took option 1, but I show you in my next post, how you could do it from code.

February 27, 2018

Copy an XsltListViewWebPart from another SharePoint Site via PowerShell – The client-side solution

Filed under: Managed Client OM, PowerShell, SP 2013 — Tags: , , — Peter Holpar @ 21:14

In my recent post I’ve illustrated, how to display SharePoint lists from other sites via PowerShell. As I told you, that solution does work only if you have direct access to the SharePoint server. Based on my experience that is not always the case. In the current post I introduce you a solution that should work even in such cases. We built this solution on the Managed Client-Object Modell of SharePoint.

Since I knew, that the CreateWebPartFromList method of the Microsoft.SharePoint.WebPartPages.SPWebPartManager is not accessible via the client object model, I first planned to apply the another approach: export the source web part via a LimitedWebPartManager instance (the client-side equivalent of SPLimitedWebPartManager), then use another LimitedWebPartManager instance to import it onto the target page. BUT (there is almost always a but…) it turned out, that although LimitedWebPartManager supports the ImportWebPart method, the ExportWebPart method is not available on the client-side (Note: as Waldek Mastykarz reported, the ExportWebPart method should be available since the March 2016 SharePoint Online CSOM update). So I came up with a fall-back plan and exported the web part by calling the exportwp.aspx page as described here by Anatoly Mironov.

As we export and import the web part from / to another sites, we create to different context objects to access them.

We read the response from the exportwp.aspx page as XML, and set the WebId property according to the ID of the source web site. There is apparently an issue with the ViewGuid property (more about them here), so we have to append it, for example, by cloning an existing XML node, like the one for the WebId property. A bit dirty workaround, but seems to work at me…

Finally, we import the web part to the target page and add it to the web part zone / position we wish.

  1. $sourceWebUrl = "http://YourSharePoint/Site1/Site2&quot;
  2. $listTitle = "YourList"
  3. $viewTitle = "YourView" # name of the view, like "All Items"
  4.  
  5. # set the path according the location of the assemblies
  6. Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
  7. Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
  8.  
  9. $clientContext = New-Object Microsoft.SharePoint.Client.ClientContext($sourceWebUrl)
  10.  
  11. $sourceWeb = $clientContext.Web
  12. $list = $sourceWeb.Lists.GetByTitle($listTitle)
  13. $views = $list.Views
  14.  
  15. $clientContext.Load($sourceWeb)
  16. $clientContext.Load($list)
  17. $clientContext.Load($views)
  18. $clientContext.ExecuteQuery()
  19.  
  20. $sourceWebId = $sourceWeb.Id
  21.  
  22. $view = $views | ? { $_.Title -eq $viewTitle }
  23.  
  24. $targetWebUrl = "http://YourSharePoint/Site1&quot;
  25. # This path should be the site relative URL of the page. If you have the page sub webs, include them in the path
  26. $targetPageSiteRelUrl = "/Site1/SitePages/SubSiteLisTest.aspx"
  27. $targetWebPartZoneId = "Bottom" # change the ID of the web part zone to match your needs
  28. $targetWebPartIndex = 0 # the intended position of the web part in the zone
  29.  
  30.  
  31. if (!$view.ServerObjectIsNull)
  32. {
  33.     Write-Host View found, exporting WebPart…
  34.  
  35.     $file = $list.RootFolder.Files.GetByUrl($view.ServerRelativeUrl)
  36.     $clientContext.Load($file)
  37.  
  38.     $webPartManager = $file.GetLimitedWebPartManager([Microsoft.SharePoint.Client.WebParts.PersonalizationScope]::Shared)
  39.     $webParts = $webPartManager.WebParts
  40.     $clientContext.Load($webParts)
  41.     $clientContext.ExecuteQuery()
  42.  
  43.     # I assume there is a single web part on the view page
  44.     # if this assumption is false, you should filter the web parts first
  45.     $webPart = $webParts[0]
  46.     # https://www.red-gate.com/simple-talk/blogs/getting-the-absolute-url-of-a-file-in-csom/
  47.     $viewAbsoluteUrl = (New-Object System.Uri($clientContext.Url)).GetLeftPart([System.UriPartial]::Authority) + $view.ServerRelativeUrl
  48.  
  49.     $exportWPUrl = $sourceWebUrl + "/_vti_bin/exportwp.aspx?pageurl=" + [System.Web.HttpUtility]::UrlEncode($viewAbsoluteUrl) + "&guidstring=" + $webPart.Id
  50.  
  51.     $request = [System.Net.WebRequest]::Create($exportWPUrl)
  52.     $request.UseDefaultCredentials = $true
  53.  
  54.     $response = $request.GetResponse()
  55.     $reader = New-Object System.IO.StreamReader $response.GetResponseStream()
  56.  
  57.     $wpXml = [Xml]$reader.ReadToEnd()
  58.     $properties = $wpXml.webParts.webPart.data.properties
  59.     $webIdProp = $properties.property | ? { $_.name -eq "WebId" }
  60.     $webIdProp.InnerText = $sourceWebId
  61.  
  62.     # "For example, setting the ViewGuid or the Toolbar properties doesn't do anything!" see:
  63.     # http://blog.bonzai-intranet.com/analysthq/2014/10/adding-an-xsltlistviewwebpart-with-a-custom-view-using-javascript/
  64.     # as a workaround for the missing ViewGuid property, clone the WebId property and change its name / type / value
  65.     $viewIdProp = $webIdProp.Clone()
  66.     $viewIdProp.name = "ViewGuid"
  67.     $viewIdProp.type = "string"    
  68.     $viewIdProp.InnerText = $view.ID.ToString("B").ToUpper() # "convert to a format like {8C1D2A1A-5BE8-469D-806E-2112965D2C1C}"
  69.  
  70.     [Void]$properties.AppendChild($viewIdProp)  
  71.  
  72.     $wpText = $wpXml.OuterXml
  73.  
  74.     Write-Host Export completed
  75.  
  76.     Write-Host Importing WebPart…
  77.     
  78.     $targetClientContext = New-Object Microsoft.SharePoint.Client.ClientContext($targetWebUrl)
  79.     $targetFile = $targetClientContext.Web.GetFileByServerRelativeUrl($targetPageSiteRelUrl)
  80.  
  81.     $targetWebPartManager = $targetFile.GetLimitedWebPartManager([Microsoft.SharePoint.Client.WebParts.PersonalizationScope]::Shared)
  82.     # note the difference:
  83.     # the server-side version of ImportWebPart returns a WebPart
  84.     # the client-side equvalent of ImportWebPart returns a WebPartDefinition
  85.     $targetWebPartDef = $targetWebPartManager.ImportWebPart($wpText)
  86.     # you could set optionally the WebId and ViewGuid properties at this point as well, but be aware of the issue with the ViewGuid property I mentioned above..
  87.     #$targetWebPartDef.WebPart.Properties["WebId"] = $sourceWebId
  88.     #$targetWebPartDef.WebPart.Properties["ViewGuid"] = $view.ID
  89.     # note the difference:
  90.     # the server-side version of AddWebPart returns void
  91.     # the client-side equvalent of AddWebPart returns a WebPartDefinition
  92.     [Void]$targetWebPartManager.AddWebPart($targetWebPartDef.WebPart, $targetWebPartZoneId, $targetWebPartIndex)
  93.     # or if you need the WebPartDefinition later, you can use these lines
  94.     #$targetWebPartDef = $targetWebPartManager.AddWebPart($targetWebPartDef.WebPart, $targetWebPartZoneId, $targetWebPartIndex)
  95.     #$targetClientContext.Load($targetFile)
  96.     $targetClientContext.ExecuteQuery()
  97.     
  98.     # write code to check in / publish the page here as required
  99.  
  100.     Write-Host Import completed
  101.  
  102. }
  103. else
  104. {
  105.     Write-Host View $viewTitle not found
  106. }
  107.  
  108.  
  109. # http://wvg-epm01e.sv-services.at/Test-Site2/_vti_bin/exportwp.aspx?pageurl=/Test-Site2/Lists/TestList/test.aspx&guidstring=8c1d2a1a-5be8-469d-806e-2112965d2c1c

That’s it, you should now be able to copy list views (XsltListViewWebPart web parts) from one SharePoint site to another from client-side via PowerShell. Of course, that is limited to a site collection scope, and there are still known issues with copying list views (generally, not limited to the PowerShell solutions), for example, copying views for a document library with a folder structure seems not to work if  you copy it from a parent site to a sub site. More about that eventually later, as soon as I collect a bit more background information about the problem.

Paging through the Entities returned by the ProjectData OData Interface of Project Server using PowerShell

Filed under: OData, PowerShell, Project Server, REST — Tags: , , , — Peter Holpar @ 21:13

About a year ago I wrote about how you can use PowerShell and Project Server REST interfaces (ProjectServer and ProjectData) to generate reports. However, at the end of last year we had a problem with the script that query the projects via the ProjectData interface:

  1. $url = 'http://YourProjectServer/PWA/_api/ProjectData/%5Ben-US%5D/Projects?$select=ProjectId,ProjectName,ProjectCreatedDate&#039;
  2. $reportPath = "C:\Data\ProjServerReports\"
  3.  
  4. $request = [System.Net.WebRequest]::Create($url)
  5. $request.UseDefaultCredentials = $true
  6. $request.Accept = "application/json;odata=verbose"
  7.  
  8. $response = $request.GetResponse()
  9. $reader = New-Object System.IO.StreamReader $response.GetResponseStream()
  10. $data = $reader.ReadToEnd()
  11.  
  12. $result = ConvertFrom-Json -InputObject $data
  13. $result.d.results | select ProjectId, ProjectName, ProjectCreatedDate | Export-Csv -Path ($reportPath + 'ProjectsFromProjectDataNoBatching.csv') -Delimiter ";" -Encoding UTF8 -NoTypeInformation

Important! You should use single quotes around the URL in this code snippet and in the other ones below to avoid PowerShell to remove the string $select in the REST query. More about that here.

A user reported an error that some of the projects were not included in the results. We found that the count of projects included in the report via ProjectServer was greater than the count of the projects we queried via  ProjectData. It is the script version that use the ProjectServer interface:

  1. $url = 'http://YourProjectServer/PWA/_api/ProjectServer/Projects?$select=Id,Name,CreatedDate&#039;
  2. $reportPath = "C:\Data\ProjServerReports\"
  3.  
  4. $request = [System.Net.WebRequest]::Create($url)
  5. $request.UseDefaultCredentials = $true
  6. $request.Accept = "application/json;odata=verbose"
  7.  
  8. $response = $request.GetResponse()
  9. $reader = New-Object System.IO.StreamReader $response.GetResponseStream()
  10. $data = $reader.ReadToEnd()
  11.  
  12. $result = ConvertFrom-Json -InputObject $data
  13. $result.d.results | select Id, Name, CreatedDate | Export-Csv -Path ($reportPath + 'ProjectsFromProjectServer.csv') -Delimiter ";" -Encoding UTF8 -NoTypeInformation

First I thought it is an issue with some permissions, but it was suspicious, that all of the missing projects were at the end of the project list (when ordered alphabetically), and the number of included project was exactly 100 in the ProjectData-based report.

Then I found this statement in the ProjectData – Project OData service reference:

“There are limits to the number of entities that can be returned in one query of the ProjectData service.”

What a surprise, the default limit for projects in the on-premise version we have is exactly 100!

You can use the Set-SPProjectOdataConfiguration PowerShell cmdlet for on-premise instances of Project Server to override the default query page size limits for any specified entity set.

Set-SPProjectOdataConfiguration -EntitySetName Projects -PageSizeOverride 200

However, it is a global change on the server, that affects all queries of the specific entity type, that can adversely the server performance, and as such, is not recommended. Instead of this, the suggested solution to query all instance of a given entity type is to implement paging by sending multiple queries using the $top and $skip operator.

Instead of a fix URL we define only a formatting pattern with placeholders for the $skip and $top parameters for the subsequent queries. More about this format is available here.

By applying the $inlinecount=allpages in the query we can assure that the result is always returned in $result.d.results as described in this post.

In the script we assume that the default limit of 100 project / query has not been changed. Note, that we use the $firstBatch variable to decide, if we should append the data to the existing report file in the Export-Csv cmdlet, or to create a new one.

In the first version of the script we process the results (output them into a CSV) immediately after receiving the response.

  1. $urlPattern = 'http://YourProjectServer/PWA/_api/ProjectData/%5Ben-US%5D/Projects?$select=ProjectId,ProjectName,ProjectCreatedDate&$skip={0}&$top={1}&$inlinecount=allpages'
  2. $reportPath = "C:\Data\ProjServerReports\"
  3.  
  4. $firstItem = 0
  5. $batchSize = 100
  6. $firstBatch = $true
  7.  
  8. Do
  9. {
  10.   Write-Host Requesting next batch of $batchSize items, starting at $firstItem
  11.   $url = $urlPattern -f $firstItem, $batchSize
  12.   Write-Host $url
  13.   $request = [System.Net.WebRequest]::Create($url)
  14.   $request.UseDefaultCredentials = $true
  15.   $request.Accept = "application/json;odata=verbose"
  16.  
  17.   $response = $request.GetResponse()
  18.   $reader = New-Object System.IO.StreamReader $response.GetResponseStream()
  19.   $data = $reader.ReadToEnd()
  20.  
  21.  
  22.   $result = ConvertFrom-Json -InputObject $data
  23.   $count = $result.d.results.Count
  24.   Write-Host Item count is $count
  25.   $result.d.results | select ProjectId, ProjectName, ProjectCreatedDate |
  26.       Export-Csv -Path ($reportPath + 'ProjectsFromProjectData_20171130_03.csv') -Delimiter ";" -Encoding UTF8 -NoTypeInformation -Append:(-not $firstBatch)
  27.   $firstBatch = $false
  28.   $firstItem += $batchSize
  29. }
  30. While ($count -eq $batchSize)

In the second we aggregate the results into an array, and process the results only after receiving the last response.

  1. $urlPattern = 'http://YourProjectServer/PWA/_api/ProjectData/%5Ben-US%5D/Projects?$select=ProjectId,ProjectName,ProjectCreatedDate&$skip={0}&$top={1}&$inlinecount=allpages'
  2. $reportPath = "C:\Data\ProjServerReports\"
  3.  
  4. $results = @()
  5. $firstItem = 0
  6. $batchSize = 100
  7.  
  8. Do
  9. {
  10.   Write-Host Requesting next batch of $batchSize items, starting at $firstItem
  11.   $url = $urlPattern -f $firstItem, $batchSize
  12.   Write-Host $url
  13.   $request = [System.Net.WebRequest]::Create($url)
  14.   $request.UseDefaultCredentials = $true
  15.   $request.Accept = "application/json;odata=verbose"
  16.  
  17.   $response = $request.GetResponse()
  18.   $reader = New-Object System.IO.StreamReader $response.GetResponseStream()
  19.   $data = $reader.ReadToEnd()
  20.  
  21.   $result = ConvertFrom-Json -InputObject $data
  22.   $count = $result.d.results.Count
  23.   Write-Host Item count is $count
  24.   $results += $result.d.results
  25.   $firstItem += $batchSize
  26. }
  27. While ($count -eq $batchSize)
  28.  
  29. $results | select ProjectId, ProjectName, ProjectCreatedDate |
  30.       Export-Csv -Path ($reportPath + 'ProjectsFromProjectDataWithBatching.csv') -Delimiter ";" -Encoding UTF8 -NoTypeInformation

Although in this case both of the scripts have the same result, you can use the second version if you need the complete list of the projects at a later time, for example, you would like to extend it with data not available at the time of the query.

Copy an XsltListViewWebPart from another SharePoint Site via PowerShell

Filed under: PowerShell, SP 2013 — Tags: , — Peter Holpar @ 21:02

It’s a common request, that the user would like to display a SharePoint List / Document library from another site. Unfortunately, that is not so easy. Users see only the lists exist on the current site when they want to add a web part to a page via the browser. Although the XsltListViewWebPart (the web part that is responsible for rendering most of the lists views with a very few exceptions, like Calendar views that still use the good-old ListViewWebPart web part you might know from SharePoint 2003) has a property WebId, you can not change it in the browser by editing the web part, just like you can not change its ListId property. If you would like to display a list on a page that belongs to another site as the one where the list exist, you can either use SharePoint Designer to copy the web part from the page of the list view to the target page, and extend it with the WebId property (ID of the source site), or export the web part from the list view, edit it, and upload to the target page as described in this post. Both of these methods require a certain amount of manual actions and as such are error-prone.

Instead of the above methods, I prefer to use PowerShell scripts to perform the job. There are multiple options to achieve that. The trivial one is to export the source web part via a SPLimitedWebPartManager instance, then use another SPLimitedWebPartManager instance to import it onto the target page, and set its WebId property:

  1. $sourceWebUrl = "http://YourSharePoint/Site1/Site2&quot;
  2. $listTitle = "YourList"
  3. $viewTitle = "YourView" # name of the view, like "All Items"
  4.  
  5. $sourceWeb = Get-SPWeb $sourceWebUrl
  6. $sourceWebId = $sourceWeb.Id
  7. $list = $sourceWeb.Lists[$listTitle]
  8. $view = $list.Views | ? { $_.Title -eq $viewTitle }
  9.  
  10. $targetWebUrl = "http://YourSharePoint/Site1&quot;
  11. # This path should be the site relative URL of the page. If you have the page sub webs, include them in the path
  12. $targetPageSiteRelUrl = "/Site1/SitePages/SubSiteLisTest.aspx"
  13. $targetWebPartZoneId = "FullPage" # change the ID of the web part zone to match your needs
  14. $targetWebPartIndex = 0 # the intended position of the web part in the zone
  15. $targetWeb = Get-SPWeb $targetWebUrl
  16.  
  17. if ($view)
  18. {
  19.     $viewPageUrl = $sourceWeb.Url + '/' + $view.Url
  20.     $webPartManager = $web.GetLimitedWebPartManager($viewPageUrl, [System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared)
  21.     $webPart = $webPartManager.WebParts[0]
  22.     # I assumed there is a single web part on the view page,
  23.     # otherwise you should find it first, like:
  24.     # $webPart = $webPartManager.WebParts | ? { $_; $_.Title -eq $listName }
  25.     if ($webPart -ne $Null) {
  26.         Write-Host WebPart found, exporting…
  27.         $webPart.ExportMode = "All"
  28.  
  29.         # use XmlTextWriter, not XmlWriter!
  30.         # https://social.msdn.microsoft.com/Forums/office/en-US/16391ce2-0454-44bc-b0db-c184898c588e/splimitedwebpartmanagerexportwebpartwebpart-xmlwriter-throws-xmlexception
  31.         $sw = New-Object System.IO.StringWriter
  32.         $xw = New-Object System.Xml.XmlTextWriter($sw)
  33.         $webPartManager.ExportWebPart($webPart, $xw);
  34.         $xw.Flush()
  35.         $xw.Close()
  36.         $wpText = $sw.ToString()
  37.  
  38.         # optionally, we could replace the WebId="00000000-0000-0000-0000-000000000000" in the text itself, like
  39.         #$wpText = $wpText.Replace('WebId="' + [Guid]::Empty + '"', 'WebId="' + $sourceWebId.Guid + '"')
  40.         # but I think setting the WebId property of the WebPart (see below) is more reliable
  41.         Write-Host Export completed
  42.  
  43.         Write-Host Importing WebPart…
  44.         $targetWebPartManager = $targetWeb.GetLimitedWebPartManager($targetPageSiteRelUrl, [System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared)
  45.         # https://stackoverflow.com/questions/4518544/xmlreader-from-a-string-content
  46.         $sr = New-Object System.IO.StringReader($wpText)
  47.         $xr = [System.Xml.XmlReader]::Create($sr)
  48.         # http://www.sites.se/2013/04/webpart-powershell-sharepoint-2013/
  49.         $errorMessage = $null
  50.         $targetWebPart = $targetWebPartManager.ImportWebPart($xr, [ref] $errorMessage)
  51.         $targetWebPart.WebId = $sourceWebId
  52.         $targetWebPart.ViewGuid = $view.ID
  53.         $targetWebPartManager.AddWebPart($targetWebPart, $targetWebPartZoneId, $targetWebPartIndex)
  54.         # check in / publish as required
  55.         Write-Host Import completed
  56.     }
  57.     else
  58.     {
  59.         Write-Host WebPart not found
  60.     }
  61. }
  62. else
  63. {
  64.     Write-Host View $viewTitle not found
  65. }

Using the same method (of course without setting the WebId property) you can copy another type of web parts as well between sites.

Probably a bit less know, and XsltListViewWebPart / ListViewWebPart specific option is to use the static CreateWebPartFromList method of the Microsoft.SharePoint.WebPartPages.SPWebPartManager class to create an instance of the source web part. After setting its WebId property, you can import the web part using a SPLimitedWebPartManager instance as earlier. The code below assumes you need the web part that belongs to the default list view:

  1. $sourceWebUrl = "http://YourSharePoint/Site1/Site2&quot;
  2. $listTitle = "YourList"
  3.  
  4. $sourceWeb = Get-SPWeb $sourceWebUrl
  5. $sourceWebId = $sourceWeb.Id
  6. $list = $sourceWeb.Lists[$listTitle]
  7.  
  8. $targetWebUrl = "http://YourSharePoint/Site1&quot;
  9. # This path should be the site relative URL of the page. If you have the page sub webs, include them in the path
  10. $targetPageSiteRelUrl = "/Site1/SitePages/SubSiteLisTest.aspx"
  11. $targetWebPartZoneId = "Bottom" # change the ID of the web part zone to match your needs
  12. $targetWebPartIndex = 0 # the intended position of the web part in the zone
  13. $targetWeb = Get-SPWeb $targetWebUrl
  14.  
  15. if ($list)
  16. {
  17.         Write-Host Source list found, importing WebPart…
  18.  
  19.         $targetWebPart = [Microsoft.SharePoint.WebPartPages.SPWebPartManager]::CreateWebPartFromList($list, $false)
  20.         $targetWebPart.WebId = $sourceWebId
  21.         $targetWebPartManager = $targetWeb.GetLimitedWebPartManager($targetPageSiteRelUrl, [System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared)
  22.         $targetWebPartManager.AddWebPart($targetWebPart, $targetWebPartZoneId, $targetWebPartIndex)
  23.         # check in / publish as required
  24.         Write-Host Import completed
  25.  
  26. }
  27. else
  28. {
  29.     Write-Host List $listTitle not found
  30. }

If you would like to copy another view, not the default one, you should get the ID of the view, and set the ViewGuid property of the web part as well:

  1. $sourceWebUrl = "http://YourSharePoint/Site1/Site2&quot;
  2. $listTitle = "YourList"
  3. $viewTitle = "YourView"
  4.  
  5. $sourceWeb = Get-SPWeb $sourceWebUrl
  6. $sourceWebId = $sourceWeb.Id
  7. $list = $sourceWeb.Lists[$listTitle]
  8. $view = $list.Views | ? { $_.Title -eq $viewTitle }
  9.  
  10. $targetWebUrl = "http://YourSharePoint/Site1&quot;
  11. # This path should be the site relative URL of the page. If you have the page sub webs, include them in the path
  12. $targetPageSiteRelUrl = "/Site1/SitePages/SubSiteLisTest.aspx"
  13. $targetWebPartZoneId = "Bottom" # change the ID of the web part zone to match your needs
  14. $targetWebPartIndex = 0 # the intended position of the web part in the zone
  15. $targetWeb = Get-SPWeb $targetWebUrl
  16.  
  17. if ($view)
  18. {
  19.         Write-Host Source list and view found, importing WebPart…
  20.  
  21.         $targetWebPart = [Microsoft.SharePoint.WebPartPages.SPWebPartManager]::CreateWebPartFromList($list, $false)
  22.         $targetWebPart.WebId = $sourceWebId
  23.         $targetWebPart.ViewGuid = $view.ID
  24.         $targetWebPartManager = $targetWeb.GetLimitedWebPartManager($targetPageSiteRelUrl, [System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared)
  25.         $targetWebPartManager.AddWebPart($targetWebPart, $targetWebPartZoneId, $targetWebPartIndex)
  26.         # check in / publish as required
  27.         Write-Host Import completed
  28.  
  29. }
  30. else
  31. {
  32.     Write-Host View $viewTitle not found
  33. }

At this point you might say: Wait a moment, that is nice, but using the original methods, like SharePoint Designer or via the browser, I was able to perform this task even if I have no direct access to the server. That is definitely not applies to the PowerShell scripts above. You are right, we identified this handicap as well, and came up with a version that works even from a client computer. I show you this version in a forthcoming post, so stay tuned!

November 23, 2017

Displaying Filtered or Sorted SharePoint Web Properties from PowerShell

Filed under: PowerShell, SP 2013, Tips & Tricks — Tags: , , — Peter Holpar @ 22:47

Specific SharePoint applications and services as well as tools, like SharePoint Designer tend to store their own settings in the property bag of web sites. Especially root webs of site collection have a lot of properties.

You can display the web properties from PowerShell like:

$web = Get-SPWeb http://YourSharePointSite
$web.AllProperties

The problem, that the above script displays really all of the properties, and the list is not alphabetically sorted, so it might be challenging  to find a specific property.

The properties and their values are stored as key-value pairs in a Hashtable object, you can filter these entries as described in this thread. For example, the script below displays the search related entries, the ones whose names begin with SRCH.

$web.AllProperties.GetEnumerator() | ? Key -like ‘SRCH*’

image

Although it is not so common, you can filter the entries based on its values as well. For example, this script display all properties having a value of True:

$web.AllProperties.GetEnumerator() | ? Value -eq ‘True’

If all you want is to display the properties sorted alphabetically by their names, it is easy to achieve either:

$web.AllProperties.GetEnumerator() | Sort-Object -Property Key

image

Of course, all of these possibilities apply not only to the property bags of web objects (SPWeb.AllProperties), but to other property bags as well, like the properties of the folders (SPFolder.Properties) and files (SPFile.Properties) or lists (SPLists.Properties) and list items (SPListItem.Properties).

Older Posts »

Create a free website or blog at WordPress.com.