Second Life of a Hungarian SharePoint Geek

February 19, 2015

How to Programmatically “Enable reporting of offensive content” on a community site

Filed under: PowerShell, SP 2013 — Peter Holpar @ 23:10

Recently a question was posted on sharepoint.stackexchange.com about how to “Enable reporting of offensive content” from code on SharePoint 2013 community sites.

image

The Community Settings page (CommunitySettings.aspx) has a code behind class Microsoft.SharePoint.Portal.CommunitySettingsPage. In its BtnSave_Click method it depends on the static EnableDisableAbuseReports method of the internal FunctionalityEnablers class to perform the actions required for reporting of offensive content. Furthermore, it sets the value of the vti_CommunityEnableReportAbuse web property to indicate that reporting is enabled for the community site.

To perform the same actions from PowerShell I wrote this script:

$site = New-Object Microsoft.SharePoint.SPSite("http://YourServer/CommunitySite")
$web = $site.OpenWeb()

# this command has only effect to the check box on the Community Settings page
$web.AllProperties["vti_CommunityEnableReportAbuse"] = "true"
$web.Update()

# the functionality itself is activated by the code below
# get a reference for the Microsoft.SharePoint.Portal assembly
$spPortalAssembly = [AppDomain]::CurrentDomain.GetAssemblies() | ? { $_.Location -ne $Null -And $_.Location.Split(‘\\’)[-1].Equals(‘Microsoft.SharePoint.Portal.dll’) }
$functionalityEnablersType = $spPortalAssembly.GetType("Microsoft.SharePoint.Portal.FunctionalityEnablers")
$mi_EnableDisableAbuseReports = $functionalityEnablersType.GetMethod("EnableDisableAbuseReports")
$mi_EnableDisableAbuseReports.Invoke($null, @($spWeb, $True))

Note: If you use “false” instead of “true” when setting the value of vti_CommunityEnableReportAbuse, and $False instead of $True when invoking the static method in the last line of code, then you can inactivate the reporting for the site.

The alternative solution is to use the server side API from C#:

web.AllProperties["vti_CommunityEnableReportAbuse"] = "true";
web.Update();

// get an assembly reference to "Microsoft.SharePoint.Portal" via an arbitrary public class from the assembly
Assembly spPortalAssembly = typeof(Microsoft.SharePoint.Portal.PortalContext).Assembly;

Type functionalityEnablersType = spPortalAssembly.GetType("Microsoft.SharePoint.Portal.FunctionalityEnablers");
MethodInfo mi_EnableDisableAbuseReports = functionalityEnablersType.GetMethod("EnableDisableAbuseReports");
mi_EnableDisableAbuseReports.Invoke(null, new object[] { web, true });

We can verify the effect of our code by checking the columns in the first view of the Discussions List before enabling the reporting via this PowerShell script:

$list = $web.Lists["Discussions List"]
$list.Views[0].ViewFields

I found these ones:

Threading
CategoriesLookup
Popularity
DescendantLikesCount
DescendantRatingsCount
AuthorReputationLookup
AuthorNumOfRepliesLookup
AuthorNumOfPostsLookup
AuthorNumOfBestResponsesLookup
AuthorLastActivityLookup
AuthorMemberSinceLookup
AuthorMemberStatusIntLookup
AuthorGiftedBadgeLookup
LikesCount
LikedBy

And after we enable reporting. Four further columns should be appended to the list of view fields if the code succeeded:

AbuseReportsCount
AbuseReportsLookup
AbuseReportsReporterLookup
AbuseReportsCommentsLookup

Or we can invoke the static IsReportAbuseEnabled method of the internal Microsoft.SharePoint.Portal.CommunityUtils class to verify if reporting is enabled, just as the OnLoad method of the Microsoft.SharePoint.Portal.CommunitySettingsPage does. You should know however, that this method does not more as simply to check the value of the vti_CommunityEnableReportAbuse web property, so even if it returns true, it does not mean for sure that reporting is really enable. So I prefer checking the columns in view as shown earlier.

The PowerShell version:

$site = New-Object Microsoft.SharePoint.SPSite("http://YourServer/CommunitySite")
$web = $site.OpenWeb()

$communityUtilsType = $spPortalAssembly.GetType("Microsoft.SharePoint.Portal.CommunityUtils")
$mi_IsReportAbuseEnabled = $communityUtilsType.GetMethod("IsReportAbuseEnabled")
$mi_IsReportAbuseEnabled.Invoke($null, @($spWeb))

The C# version:

// get an assembly reference to "Microsoft.SharePoint.Portal" via an arbitrary public class from the assembly
Assembly spPortalAssembly = typeof(Microsoft.SharePoint.Portal.PortalContext).Assembly;

Type communityUtilsType = spPortalAssembly.GetType("Microsoft.SharePoint.Portal.CommunityUtils");
MethodInfo mi_IsReportAbuseEnabled = communityUtilsType.GetMethod("IsReportAbuseEnabled");
mi_IsReportAbuseEnabled.Invoke(null, new object[] { web });

February 10, 2015

Further Effects of Running Code in Elevated Privileges Block

Filed under: Permissions, PowerShell, SP 2010, SP 2013 — Tags: , , , — Peter Holpar @ 23:29

A few days ago I already published a blog post about the effects of running your PowerShell code in an elevated privileges block. In the past days I made some further tests to check what kind of effect the permission elevation might have if the code runs out of the SharePoint web application context, for example, in the case of server side console applications, windows services or PowerShell scripts, just to name a few typical cases.

To test the effects, I’ve created a simple console application in C#, and a PowerShell script.

I’ve tested the application / script with two different permissions. In both cases, the user running the application was neither site owner (a.k.a. primary administrator) nor a secondary administrator. In the first case, the user has the Full Control permission level on the root web of the site collection, in the second case the user has no permissions at all.

In C# I’ve defined a CheckIfCurrentUserIsSiteAdmin method as:

  1. private void CheckIfCurrentUserIsSiteAdmin(string url)
  2. {
  3.     using (SPSite site = new SPSite(url))
  4.     {
  5.         using (SPWeb web = site.OpenWeb())
  6.         {
  7.             SPUser currentUser = web.CurrentUser;
  8.             Console.WriteLine("Current user ({0}) is site admin on '{1}': {2}", currentUser.LoginName, url, currentUser.IsSiteAdmin);
  9.             Console.WriteLine("Current user ({0}) is site auditor on '{1}': {2}", currentUser.LoginName, url, currentUser.IsSiteAuditor);
  10.             Console.WriteLine("Effective permissions on web: '{0}'", web.EffectiveBasePermissions);
  11.             try
  12.             {
  13.                 Console.WriteLine("web.UserIsWebAdmin: '{0}'", web.UserIsWebAdmin);
  14.             }
  15.             catch (Exception ex)
  16.             {
  17.                 Console.WriteLine("'web.UserIsWebAdmin' threw an exception: '{0}'", ex.Message);
  18.             }
  19.             try
  20.             {
  21.                 Console.WriteLine("web.UserIsSiteAdmin: '{0}'", web.UserIsSiteAdmin);
  22.             }
  23.             catch (Exception ex)
  24.             {
  25.                 Console.WriteLine("'web.UserIsSiteAdmin' threw an exception: '{0}'", ex.Message);
  26.             }
  27.         }
  28.     }
  29. }

Then called it without and with elevated permissions:

  1. string url = http://YourServer;
  2. Console.WriteLine("Before elevation of privileges");
  3. CheckIfCurrentUserIsSiteAdmin(url);
  4. Console.WriteLine("After elevation of privileges");
  5. SPSecurity.RunWithElevatedPrivileges(
  6.     () =>
  7.         {
  8.             CheckIfCurrentUserIsSiteAdmin(url);
  9.         });

The summary of the result:

If the user executing the application has full permission on the (root) web (Full Control permission level):

Before elevation:
Current user is site admin: False
Effective perm.: FullMask
web.UserIsWebAdmin: True
web.UserIsSiteAdmin: False

After elevation:
Current user is site admin: True
Effective perm.: FullMask
web.UserIsWebAdmin: True
web.UserIsSiteAdmin: True

If the user has no permission on the (root) web:

Before elevation:
Current user is site admin: False
Effective perm.: EmptyMask
web.UserIsWebAdmin: ‘Access denied’ exception when reading the property
web.UserIsSiteAdmin: ‘Access denied’ exception when reading the property

After elevation:
Current user is site admin: True
Effective perm.: FullMask
web.UserIsWebAdmin: True
web.UserIsSiteAdmin: True

In PowerShell I defined a CheckIfCurrentUserIsSiteAdmin method as well, and invoked that without and with elevated right:

function CheckIfCurrentUserIsSiteAdmin($url) {
  $site = Get-SPSite $url 
  $web = $site.RootWeb
  $currentUser = $web.CurrentUser
  Write-Host Current user $currentUser.LoginName is site admin on $url : $currentUser.IsSiteAdmin
  Write-Host Current user $currentUser.LoginName is site auditor on $url : $currentUser.IsSiteAuditor

  Write-Host Effective permissions on web: $web.EffectiveBasePermissions
  Write-Host web.UserIsWebAdmin: $web.UserIsWebAdmin
  Write-Host web.UserIsSiteAdmin: $web.UserIsSiteAdmin
}

$url = "http://YourServer"

Write-Host Before elevation of privileges
CheckIfCurrentUserIsSiteAdmin $url

Write-Host After elevation of privileges
[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges(
  {
    CheckIfCurrentUserIsSiteAdmin $url
  }
)

The results were the same as in the case of the C# console application, except the web.UserIsWebAdmin and web.UserIsSiteAdmin, when calling with no permissions on the web. In case we don’t receive any exception, simply no value (neither True nor False) was returned.

These results show, that any code, let it be a method in a standard SharePoint API, or a custom component, that depends on the above tested properties, behaves differently when using with elevated privileges, even if it is executed from an application external to the SharePoint web application context, that means, even if the identity of the process does not change.

February 9, 2015

“Decoding” SharePoint Error Messages using PowerShell

Filed under: PowerShell, SP 2010, Tips & Tricks — Tags: , , — Peter Holpar @ 22:26

When working with SharePoint errors in ULS logs, you can find the error message near to the stack trace. In case of simple methods the stack trace may be enough to identify the exact conditions under which the exception was thrown. However, if the method is complex, with a lot of conditions and branches, it is not always trivial to find the error source, as we don’t see the exception message itself, as it is stored in language-specific resource files, and you see only a kind of keyword in the code.

For example, let’s see the GetItemById method of the SPList object with this signature:

internal SPListItem GetItemById(string strId, int id, string strRootFolder, bool cacheRowsetAndId, string strViewFields, bool bDatesInUtc)

There is a condition near to the end of the method:

if (this.IsUserInformationList)
{
    throw new ArgumentException(SPResource.GetString("CannotFindUser", new object[0]));
}
throw new ArgumentException(SPResource.GetString("ItemGone", new object[0]));

How could we “decode” this keyword to the real error message? It is easy to achieve using PowerShell.

For example, to get the error message for the “ItemGone”:

[Microsoft.SharePoint.SPResource]::GetString("ItemGone")

is

Item does not exist. It may have been deleted by another user.

Note, that since the second parameter is an empty array, we can simply ignore it when invoking the static GetString method.

If you need the language specific error message (for example, the German one):

$ci = New-Object System.Globalization.CultureInfo("de-de")
[Microsoft.SharePoint.SPResource]::GetString($ci, "ItemGone")

it is

Das Element ist nicht vorhanden. Möglicherweise wurde es von einem anderen Benutzer gelöscht.

Having the error message, it is already obvious most of the time, at which line of code the exception was thrown.

It can also help to translate the localized message to the English one, and use it to look up a solution for the error on the Internet using your favorite search engine, as there are probably more results when you search for the English text.

Setting the Value of an URL Field or other Complex Field Types using PowerShell via the Managed Client Object Model

Filed under: Managed Client OM, PowerShell, SP 2013 — Tags: , , — Peter Holpar @ 00:30

Assume you have a SharePoint list that includes fields of type Hyperlink or Picture (name this field UrlField), Person or Group (field name User) and Lookup (field name Lookup) and you need to set their values remotely using PowerShell via the Managed Client Object Model.

In C# you would set the Hyperlink field using this code:

  1. var siteUrl = "http://YourServer";
  2.  
  3. using (var context = new ClientContext(siteUrl))
  4. {
  5.     var web = context.Web;
  6.     var list = web.Lists.GetByTitle("YourTestList");
  7.     var item = list.GetItemById(1);
  8.  
  9.     var urlValue = new FieldUrlValue();
  10.     urlValue.Url = "http://www.company.com";
  11.     urlValue.Description = "Description of the URL";
  12.     item["UrlField"] = urlValue;
  13.  
  14.     item.Update();
  15.     context.ExecuteQuery();
  16. }

You may think, that translating this code to PowerShell is as easy as:

Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"

$siteUrl = "http://YourServer"

$context = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl)
$web = $context.Web
$list = $web.Lists.GetByTitle("YourTestList")
$item = $list.GetItemById(1)

$urlValue = New-Object Microsoft.SharePoint.Client.FieldUrlValue
$urlValue.Url = "http://www.company.com"
$urlValue.Description = "Description of the URL"
$item["UrlField"] = $urlValue

$item.Update()
$context.ExecuteQuery()

However, that approach simply won’t work, as you receive this error:

"Invalid URL: Microsoft.SharePoint.Client.FieldUrlValue" (ErrorCode: -2130575155)

When capturing the network traffic with Fiddler, the C# version sends this for the SetFieldValue method:

image

For the PowerShell code however:

image

You can see, that the first parameter, the field name is the same in both cases, however the second parameter, that should be the value we assign to the field is wrong in the second case. In the C# case it has the correct type (FieldUrlValue , represented by the GUID value in the TypeId), however in PowerShell the type is Unspecified, and the really type is sent as text in this parameter (I assume the ToString() method was called on the type by PowerShell)

Solution: You should explicitly cast the object in the $urlValue variable to the FieldUrlValue type as shown below (the single difference is highlighted in code with bold):

Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"

$siteUrl = "http://YourServer"

$context = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl)
$web = $context.Web
$list = $web.Lists.GetByTitle("YourTestList")
$item = $list.GetItemById(1)

$urlValue = New-Object Microsoft.SharePoint.Client.FieldUrlValue
$urlValue.Url = "http://www.company.com"
$urlValue.Description = "Description of the URL"
$item["UrlField"] = [Microsoft.SharePoint.Client.FieldUrlValue]$urlValue

$item.Update()
$context.ExecuteQuery()

Similarly in case of the other complex field types (only the relevant codes are included).

For the Person or Group field in C# (assuming 2 is the ID of the user you would like to set in the field):

  1. var userValue = new FieldUserValue();
  2. userValue.LookupId = 2;
  3. item["User"] = userValue;

The network trace:

image

PowerShell code, that does not work:

$userValue = New-Object Microsoft.SharePoint.Client.FieldUserValue
$userValue.LookupId = 2
$item["User"] = $userValue

The error you receive:

"Invalid data has been used to update the list item. The field you are trying to update may be read only." (ErrorCode: -2147352571)

The network trace:

image

PowerShell code, that does work:

$userValue = New-Object Microsoft.SharePoint.Client.FieldUserValue
$userValue.LookupId = 2
$item["User"] = [Microsoft.SharePoint.Client.FieldUserValue]$userValue

For the Lookup field in C# (assuming 2 is the ID of the related list item you would like to set in the field):

  1. var lookUpValue = new FieldLookupValue();
  2. lookUpValue.LookupId = 2;
  3. item["Lookup"] = lookUpValue;

The network trace:

image

PowerShell code, that does not work:

$lookUpValue = New-Object Microsoft.SharePoint.Client.FieldLookupValue
$lookUpValue.LookupId = 2
$item["Lookup"] = $lookUpValue

The error you receive:

"Invalid data has been used to update the list item. The field you are trying to update may be read only." (ErrorCode: -2147352571)

The network trace:

image

PowerShell code, that does work:

$lookUpValue = New-Object Microsoft.SharePoint.Client.FieldLookupValue
$lookUpValue.LookupId = 2
$item["Lookup"] = [Microsoft.SharePoint.Client.FieldLookupValue]$lookUpValue

February 7, 2015

Changing Site Collection Administrators via PowerShell without Elevated Permissions

Filed under: PowerShell, Security, SP 2010 — Tags: , , — Peter Holpar @ 22:20

In my recent post I’ve already illustrated how to read and set the primary and secondary site collection administrators (the Owner and SecondaryContact properties of the corresponding SPSite object) via PowerShell. In that samples I’ve used elevated privileges to achieve my goals.

Let’s see if it is there a way without elevating the privileges.

Before PowerShell, before SharePoint 2010, the standard way to display / change the site owner and the secondary owner in command line was the stsadm command, that is still available to us.

For example, we can display this information for all site of a web application using the enumsites operation:

stsadm -o enumsites -url http://mysiteroot

To set the site owner, we can use the siteowner operation with the ownerlogin parameter:

stsadm -o siteowner -url http://mysiteroot/users/user1 -ownerlogin "domain\user1"

For the secondary admin, use the secondarylogin parameter instead of ownerlogin.

Note: You should have local administrator rights on the server where you run this commands, otherwise you receive an “Access denied.” error message. The reason, that for most of the stsadm operation, the following security check is performed in the entry method (public static int Main) of the Microsoft.SharePoint.StsAdmin.SPStsAdmin class:

if (!SPAdministrationServiceUtilities.IsCurrentUserMachineAdmin())
{
  Console.WriteLine(SPResource.GetString("AccessDenied", new object[0]));
  Console.WriteLine();
  return -2147024891;
}

I decided to check, what kind of method stsadm uses to read and change the values. The implementation of the siteowner operation can be found in the Microsoft.SharePoint.StsAdmin.SPSiteOwner class. To access the the Owner and SecondaryContact properties of the SPSite object, the OwnerLoginName and SecondaryContactLoginName properties of the SPSiteAdministration class are used. In the constructor of this class there is a security check that verify if the calling user is a farm administrator:

internal SPSiteAdministration(SPSite site)
{
    if (site == null)
    {
        throw new ArgumentNullException("site");
    }
    this.m_Site = site;
    if (this.m_Site.WebApplication.Farm.CurrentUserIsAdministrator())
    {
        this.m_Site.AdministratorOperationMode = true;
    }

To display the owner and the secondary contact of the site collection, we can use the following PowerShell script:

$url = "http://mysiteroot/users/user1"
$siteAdmin = New-Object Microsoft.SharePoint.Administration.SPSiteAdministration($url)
$siteAdmin.OwnerLoginName
$siteAdmin.SecondaryContactLoginName

Changing these values is so simple as:

$url = "http://mysiteroot/users/user1"
$siteAdmin = New-Object Microsoft.SharePoint.Administration.SPSiteAdministration($url)
$siteAdmin.OwnerLoginName = "domain\user1"
$siteAdmin.SecondaryContactLoginName = "domain\user2"

Note, that we are using the login name as string, and not an SPUser when assigning the values, and there is no need for elevated privileges. The caller must be a farm administrator for the current SharePoint farm, however, as we call this code directly, and not from stsadm, where this is checked, the user should not be a local admin.

I found it a bit inconsistent and disturbing, that we can access (read / set) the same properties via various objects and methods of the base SharePoint library, that perform various permission checks, and so one can avoid the security checks implemented in one of the objects when accessing the very same information via another class.

Changing Site Collection Administrators via PowerShell Using Elevated Permissions

Filed under: PowerShell, Security, SP 2010 — Tags: , , — Peter Holpar @ 05:06

Recently we migrated the users of a SharePoint farm into another domain. As part of the migration the MySites of the users should have been migrated as well. For the user migration we used the Move-SPUser Cmdlet, however it seems to have no effect on the site primary and secondary administrators (that means the Owner and SecondaryContact properties of the corresponding SPSite object). As you might know, each MySite is a site collection in SharePoint, the user the MySite belongs to is by default the primary site collection administrator and there is no secondary admin specified. It is possible to change (“migrate”) the admins via the Central Administration web UI, however having hundreds of users, it was not a viable option to us. But no problem, we can surely change these values via PowerShell as well, couldn’t we? Let’s test it first, and use PowerShell to read the values. We have all of the MySites in a separate web application, so we tried to iterate through all its site collections and dump out the information we need.

These samples assume that the URL of the web application (the MySite root) is http://mysiteroot, and the MySites are under the managed path /users, for example, the URL of the MySite of user1 is http://mysiteroot/users/user1.

Our first try was:

$waUrl = "http://mysiteroot"

$wa = Get-SPWebApplication $waUrl
Get-SPSite -WebApplication $wa -Limit ALL | % {
  Write-Host "Url:" $_.Url
  Write-Host "Primary Administrator:" $_.Owner.LoginName
  Write-Host "Secondary Administrator:" $_.SecondaryContact.LoginName
  Write-Host "—————————–"
}

Surprise! The URLs are dumped out, however the Owner only for the root site and the own MySite of the user who executed the script, the SecondaryContact only for the root (in the MySite of the executing user had no SecondaryContact defined). That means one could access these properties only for the site where one is defined as site collection administrator (either primary or secondary). There is no error message (like Access denied) for the other sites, simply no information about the admins displayed.

However, if we run the code in an elevated privileges block, the Owner and SecondaryContact properties are dumped out as well:

$waUrl = "http://mysiteroot"

[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges (
  {
    $wa = Get-SPWebApplication $waUrl
    Get-SPSite -WebApplication $wa -Limit ALL | % {
      Write-Host "Url:" $_.Url
      Write-Host "Primary Administrator:" $_.Owner.LoginName
      Write-Host "Secondary Administrator:" $_.SecondaryContact.LoginName
      Write-Host "—————————–"
    }
  }
)

Note 1: If you would like to dump out the info without Write-Host in a RunWithElevatedPrivileges code block, you would not see the result in the console, even the Url properties would disappear.

The following sample display no result:

$waUrl = "http://mysiteroot"

[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges (
  {
    $wa = Get-SPWebApplication $waUrl
    Get-SPSite -WebApplication $wa -Limit ALL | % {
      $_.Url
      $_.Owner.LoginName
      $_.SecondaryContact.LoginName
    }
  }
)

Note 2: You don’t need to place all of your code in the elevated block. It is enough to place the code that gets the reference to the web application (site, web, etc.) in this block.

The following code works just as well as the original one with elevated privileges:

$waUrl = "http://mysiteroot"

[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges (
  {
    $wa = Get-SPWebApplication $waUrl
  }
)

Get-SPSite -WebApplication $wa -Limit ALL | % {
  Write-Host "Url:" $_.Url
  Write-Host "Primary Administrator:" $_.Owner.LoginName
  Write-Host "Secondary Administrator:" $_.SecondaryContact.LoginName
  Write-Host "—————————–"
}

After we displayed the information about the site admins, let’s see, how can we change the configuration. First I tried without elevation, although I was already sure, that it won’t perform the requested operation. In the following samples I try to set the Owner property, but in the case of SecondaryContact it would have the same outcome.

$url = "http://mysiteroot/users/user1"
$siteOwnerLogin = "domain\user1" 
$web = Get-SPWeb $url
$user = $web.EnsureUser($siteOwnerLogin)
$site = $web.Site
$site.Owner = $user

The code above does not work. We receive an error message (Attempted to perform an unauthorized operation.), and the site owner is not changed.

However, using the elevated code block will solve the problem again:

$url = "http://mysiteroot/users/user1"
$siteOwnerLogin = "domain\user1"

[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges(
     {
         $web = Get-SPWeb $url
         $user = $web.EnsureUser($siteOwnerLogin)
         $site = $web.Site
         $site.Owner = $user
     }
)

You can find opinions on the web, like this one, that states, that running PowerShell code in elevated block has no effect at all. The samples above demonstrate however, that it really DOES have effect.

But what effect of code elevation is it, that makes it possible to get and set the Owner and SecondaryContact properties in the former samples?

If you have a look at the source code of the Owner (or SecondaryContact) property, for example, using Reflector, you will see, that there is a double permission check, both of them can throw an UnauthorizedAccessException. We can ignore the first one (that is !this.AdministratorOperationMode), while this condition is only checked if there is no CurrentUser for the RootWeb of the site. The second permission check is the important one, that is checked if the CurrentUser is not null, and it is:

(!this.RootWeb.CurrentUser.IsSiteAdmin)

If this condition is true, then an UnauthorizedAccessException is thrown. The setter method of the Owner (or SecondaryContact) property calls first the getter, so the same condition is checked in this case as well. Although the call to the getter is in a try-catch block, the catch is valid only for the type SPException, so it has no effect on the UnauthorizedAccessException thrown by the getter.

Remark: I have to admit, that it is not yet clear to me, why the exception thrown by the getter is not displayed when calling the getter from PowerShell.

Let’s see a further example of code elevation from PowerShell to clear the reason, why the former samples with elevation solved the issue of getting / setting the Owner and SecondaryContact properties:

$url = "http://mysiteroot/users/user1"
$web = Get-SPWeb $url

$userName = $web.CurrentUser.LoginName
$userIsAdmin = $web.CurrentUser.IsSiteAdmin

# elevated privilages
[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges(
     {
         $web = Get-SPWeb $url
         $userNameElevated = $web.CurrentUser
         $userIsAdminElevated = $web.CurrentUser.IsSiteAdmin
     }
)

Write-Host "Before elevation"
Write-Host "User name: " $userName
Write-Host "User is site admin: " $userIsAdmin
Write-Host "After elevation"
Write-Host "User name: " $userNameElevated
Write-Host "User is site admin: " $userIsAdminElevated

Assuming the executing user has neither owner nor secondary contact for the site collection, the code returns the same user name before and after elevation, however the value of the IsSiteAdmin property is false before the elevation, but true after the elevation. As we have seen from the previous reflectoring, this change is just enough for the getter / setter  methods of the Owner and SecondaryContact properties to work.

In my next post I show you an alternative method that make it possible to read / change these values from PowerShell without any kind of elevation.

February 4, 2015

How to Move all of the Site Collections of a Web Application into a Single Content Database using PowerShell

Filed under: Administration, PowerShell, SP 2010 — Tags: , , — Peter Holpar @ 23:45

Recently we had a task at one of our clients to consolidate all of the site collections of a SharePoint web application into a single content database.

There were around 10 content DBs in this web application, each of them included about 5-10 site collections. The total size of the content DBs was around 1-2 GBs, and they expected no growth in the near future.

To make the farm administration easier we had to move all of the site collections into the first content DB (let’s call it ContentDB1). We achieved this goal via the following PowerShell script:

$webAppUrl = "http://YourSPWebApp"
$destinationDBName = "ContentDB1"

$wa = Get-SPWebApplication $webAppUrl
$destinationDB = Get-SPContentDatabase -WebApplication $wa | ? { $_.Name -eq $destinationDBName }

Get-SPContentDatabase -WebApplication $wa | ? { $_.Name -ne $destinationDBName } | % {
  Get-SPSite -ContentDatabase $_ | Move-SPSite -DestinationDatabase $destinationDB -Confirm:$False
  # we can disable the content DB at the end
  Set-SPContentDatabase $_ -Status Disabled -Confirm:$False
  # or dismount it from SharePoint
  # Dismount-SPContentDatabase $_ -Confirm:$False
  # or remove it from the SharePoint server as well as from SQL server
  # Remove-SPContentDatabase $_ -Confirm:$False -Force
}

As you can see, we iterate through all site collections of all DBs in the web application (except the target content DB, ContentDB1), and move them into the ContentDB1 database using the Move-SPSite CmdLet.

At the end we can either disable the already empty content databases, dismount the from SharePoint, or remove them from the SQL server.

Don’t forget to execute IISRESET after the script to let the configuration changes be reflected in the Central Administration UI.

November 1, 2014

No more “Show More” in Tasks lists

Filed under: ListFieldIterator, PowerShell, SP 2013 — Tags: , — Peter Holpar @ 05:58

I meet frequently with the request to turn off the new “Show More” feature at the SharePoint 2013 Tasks lists. The next screenshot shows the default new task form as displayed after page load with the limited set of fields, the “Show More” button is highlighted:

image

After clicking on that button, the remaining fields are displayed, as shown below:

image

Surprisingly, the solutions I found on the web (like this, this or this one) try to solve the issue and expand the fields automatically on the client side using JavaScript – that I consider rather hacking – instead of solving the real reason of the problem.

In this blog post I would like to introduce a few other solutions to the problem, that are worth considering instead of the JavaScript approach.

As you probably know, the various list item forms (NewForm.aspx, DispForm.aspx and EditForm.aspx) use the ListFormWebPart web part to render the item in new, display and edit mode. Which rendering template the web part uses is specified by its TemplateName property. If no TemplateName is specified on the page itself for the web part, the default value is used:

image

As you can see from this code, the template is read from the configuration of the content type of the item being edited / displayed.

You can display this value in the case of a standard Tasks list using the next PowerShell script:

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["Tasks"]
$ct = $list.ContentTypes[0]
$ct.DisplayFormTemplateName

The output of this should be “TaskForm”, and you get the same result for the other two properties (NewFormTemplateName and EditFormTemplateName).

In the case of other list types the form used is the “ListForm”. So if you would like to use the standard form layout without the “Show More” button, you can simply replace the form template for the content type assigned to the list (solution 1).

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["Tasks"]
$ct = $list.ContentTypes[0]
$ct.DisplayFormTemplateName = "ListForm"
$ct.NewFormTemplateName = "ListForm"
$ct.EditFormTemplateName = "ListForm"
$ct.Update()

Note: The change affect only the given list, but no other Tasks lists, as we change the property only for the local copy of the Task content type.

Alternatively, you can open the form using SharePoint Designer, and set the TemplateName property of the ListFormWebPart web part explicitly (solution 2):

<TemplateName xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">ListForm</TemplateName>

But what’s the difference between the TaskForm and ListForm templates? Don’t we lose any functionality if we simply switch the form template? What`s included in TaskForm and what in ListForm?

These questions can be answered if we have a look at these templates in DefaultTemplates.ascx (located in folder [SharePoint Root]\TEMPLATE\CONTROLTEMPLATES).

If we look for the templates having id="ListForm" and “TaskForm”, we find that there are several differences between them. Just to name a few, the ListForm uses a standard ListFieldIterator control, while in the TaskForm we find a TaskFieldIteratorSpecifiedListFieldIterator combo, and various combinations of an EditDatesSelector control. I had not yet time to investigate the purpose of the latter one, but having a look at the code of the TaskFieldIterator and its base class DividingListFieldIterator (via Reflector or dotPeek), this control itself seems powerful enough to find an other way to eliminate the “Show More” button.

In one of my former blog posts I’ve already described the process how can we customize a standard SharePoint rendering template. A altering an out-of-the-box file is definitely not recommended, you should create a new .ascx file (for example, call it CustomTaskForm.ascx) in the CONTROLTEMPLATES folder, and copy the content of the rendering template with id="TaskForm" into the file, and include the same Control, Assembly and Register headers as found in the DefaultTemplates.ascx. Alter the id property of the template to CustomTaskForm.

Add the ShowExpanded="true" attribute to the TaskListFieldIterator control in the file (solution 3).

<SharePoint:TaskListFieldIterator ShowExpanded="true" …

Save the changes and execute IISRESET.

Open the list item forms (NewForm.aspx, DispForm.aspx and EditForm.aspx) and assign the new custom rendering template to the ListFormWebPart.

<TemplateName xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm"&gt;CustomTaskForm</TemplateName>

Save the changes again. The item should be displayed now with all its fields without the “Show More” button.

If you want to keep the button, but would like to include / exclude other fields into / from the top, you can change the value of the TopFields property. Mandatory fields of the content type and the fields specified in this property should be displayed by default without clicking on the “Show More” button.

For example, we could include the Priority field on the form, if we include ;#Priorty in this property.

<SharePoint:TaskListFieldIterator TopFields="…Title;#StartDate;#DueDate;#AssignedTo;#Priorty;#PercentComplete;#RelatedItems;#Description" runat="server"/>

Of course, in this case we should remove the ShowExpanded="true" we just included in the former step!

After saving the changes and an IISRESET, the forms should include the Priority field as well.

image

By studying the constructor of the TaskListFieldIterator class, we can find two further ways to show the form with all the fields.

We can pass the Expanded=1 in the request query string like NewForm.aspx?Expanded=1 (solution 4), or change the column order of the list content type, for example, switch the order of the Priority and Task Status fields (solution 5).

image

In both cases the form will be displayed with the fields expanded automatically. Of course, in the second case the field display order will differ from the standard one, but it’s only a minor difference.

image

I hope you find a method from the above described ones that fulfills your needs, and can easily eliminate the “Show More” button if you wish, without any kind of JavaScript magic.

September 29, 2014

Importing multi-level Lookup Tables using PowerShell

Filed under: ALM, PowerShell, Project Server, PSI — Tags: , , , — Peter Holpar @ 21:54

Recently I’m working quite a lot with Project Server 2013. My tasks include – beyond development – creation of methods that supports the continuous delivery of the results from the development environment to the test and production environments. I found that my old friend, PowerShell is an invaluable tool in this field as well.

Recently I had to solve a problem, where we had a rather complex, multi-level lookup table (RBS) on the development server, and we had to transfer the same structure on each deployment to the test server. Typing the same structure via the UI each time would have been a very boring and time consuming activity.

If we export the structure via the UI to Excel,

image

the result looks like this:

image

However, when we try to paste the values to the lookup list via the UI, the fields are shifted to the right: the values in the Level field become to the values of the Value field, the Value becomes to the Description, and the original Description is lost, making the whole pasting worthless.

image

I found a very useful PowerShell script on the blog of Paul Mather (the code is available in the TechNet Script Center as well). This script utilizes the PSI interface, however is limited to a single level of values, no hierarchical lookup tables.

I’ve extended the sample using the generic Stack object of the .NET Framework, pushing and popping the Guids of the parent items, and importing the value of the Description field as well. Otherwise most of the code was borrowed from, and the functionality is identical to the original version of Paul. As input file, a TAB separated text file is used without field name headers, including the Level, Value and Description fields, in the case above, for example:

1    Value 1    Desc 1
2    Value 1_1    Desc 1.1
3    Value 1_1_1    Desc 1.1.1
2    Value 1_2    Desc 1.2
3    Value 1_2_1    Desc 1.2.1
2    Value 1_3    Desc 1.3

This sample is limited to lookup tables with character-based code sequences.

The PowerShell script that enables the mulit-level import:

  1. #Get lookup table values to add
  2. $values = Get-Content "C:\Data\PowerShell\RBSValues.txt"
  3.  
  4. #Specify Lookup table to update
  5. $lookupTableName = "RBS"
  6. $lcid = 1033
  7. $emptyString = [String]::empty
  8. $svcPSProxy = New-WebServiceProxy -Uri "http://sp2013/pwa/_vti_bin/PSI/LookupTable.asmx?wsdl&quot; -UseDefaultCredential
  9. $lookupTableGuid = ($svcPSProxy.ReadLookupTables($emptyString, 0, $lcid).LookupTables  | ? {$_.LT_NAME -eq $lookupTableName }).LT_UID
  10. $lookupTable = $svcPSProxy.ReadLookupTablesbyUids($lookupTableGuid, 1, $lcid)
  11. #get lookup table count
  12. $lookuptableValues = $svcPSProxy.ReadLookupTablesbyUids($lookupTableGuid, 0, $lcid).LookupTableTrees
  13. $count = $lookuptableValues.Count + 1
  14. #update lookup table…
  15. $stack = New-Object System.Collections.Generic.Stack[Guid]
  16. $lastLevel = 1
  17.  
  18. $values | % {
  19.     $fields = $_ -split '\t+'
  20.         $level = $fields[0]
  21.         $text = $fields[1]
  22.         $desc = $fields[2]
  23.  
  24.     $guid = [Guid]::NewGuid()
  25.     # Write-Host Count: $count, text: $text, Guid: $guid, Level: $level, Last level: $lastLevel
  26.     $parentGuid = $lastGuid
  27.     If ($lastLevel -lt $level) {
  28.         $stack.Push($lastGuid)
  29.         # Write-Host Parent GUID Pushed: $parentGuid
  30.     }
  31.     Else {
  32.         While (($stack.Count -ge ($level)) -and ($stack.Count -gt 1)) {
  33.             # Write-Host Popping level ($stack.Count + 1)
  34.             $parentGuid = $stack.Pop()
  35.             # Write-Host Parent GUID Popped: $parentGuid
  36.         }
  37.         If ($stack.Count -gt 0) {
  38.             $parentGuid = $stack.Peek()
  39.             # Write-Host Parent GUID Peeked: $parentGuid
  40.         }
  41.     }
  42.  
  43.  
  44.     $LookupRow = $lookuptable.LookupTableTrees.NewLookupTableTreesRow()
  45.     If (-Not [String]::IsNullOrEmpty($desc)) {
  46.         $LookupRow.LT_VALUE_DESC = $desc
  47.     }
  48.     $LookupRow.LT_STRUCT_UID = $guid
  49.     $LookupRow.LT_UID = $lookupTableGuid
  50.     $LookupRow.LT_VALUE_TEXT = $text
  51.     If ($level -gt 1) {
  52.         # Write-Host Parent GUID set: $parentGuid
  53.         $LookupRow.LT_PARENT_STRUCT_UID = $parentGuid
  54.     }
  55.     $LookupRow.LT_VALUE_SORT_INDEX =  ($count++)
  56.     $lookuptable.LookupTableTrees.AddLookupTableTreesRow($LookupRow)
  57.  
  58.     $lastGuid = $guid
  59.     $lastLevel = $level
  60. }
  61.  
  62. $Error.Clear()
  63. Try
  64.     {
  65.         $svcPSProxy.UpdateLookupTables($lookuptable , 0 , 1 , $lcid)
  66.     }
  67. Catch
  68.     {
  69.         Write-Host "Error updating the Lookup table, see the error below:" -ForeGroundColor Red -BackGroundColor White
  70.         Write-Host "$error" -ForeGroundColor Red
  71.     }
  72. If ($Error.Count -eq 0)
  73.     {
  74.         Write-Host "The lookup table $lookupTablename has been updated with the values from the text file specified" -ForeGroundColor Green
  75.     }
  76. Else
  77.     {
  78.         Write-Host "The lookup table $lookupTablename has not been updated with the values from the text file specified, please see error" -ForeGroundColor Red -BackGroundColor White
  79.     }
  80. #force checkin in case of failure
  81. $Error.Clear()
  82. Try
  83.     {
  84.      $svcPSProxy.CheckInLookUpTables($lookupTableGuid, 1)
  85.     }
  86. Catch
  87.     {
  88.         If ($error -match "LastError=CICONotCheckedOut")
  89.             {
  90.     
  91.             }
  92.         Else
  93.         {
  94.             Write-Host "Error checking the Lookup table, see the error below:" -ForeGroundColor Red -BackGroundColor White
  95.             Write-Host "$error" -ForeGroundColor Red
  96.         }
  97.     }

The script includes a lot of Write-Host cmdlets to enable tracking of the process. These are commented in the version above. You are free to either use or delete these lines as you wish.

Note: Don’t forget to alter the file path, the URI and the lookup table name, and the LCID as well, if you are working with a non-English version of PWA.

August 31, 2014

How to use PowerShell to check if a SharePoint Group with a specified ID or name exists–Without error handling

Filed under: PowerShell, SP 2013, Tips & Tricks — Tags: , , — Peter Holpar @ 23:18

Recently I created a PowerShell script that should delete a group that has a specific name. If the script runs the second time, it throws an exception since the group is already deleted.

If you want to get/delete/add a group from/to a SPGroupCollection (like SiteGroups or Groups of an SPWeb) the methods throw typically exceptions of different kinds if the group does not exist (or already does exist in case of addition):

$web.SiteGroups.Remove(12345) throws
Specified argument was out of the range of valid values.

$web.SiteGroups.Remove("YourGroupName") throws
Group cannot be found.

$web.SiteGroups.GetByID(12345) and
$web.SiteGroups.GetByName("YourGroupName") throw
Group cannot be found.

$web.SiteGroups.Add("YourGroupName", $usr, $null, "Group description") throws
The specified name is already in use.

I wanted to eliminate the error messages. If these commands were PowerShell Cmdlets, we could use the common ErrorAction parameter with the value SilentlyContinue (see more here), however with standard .NET object calls only the Try/Catch block would be available.

Throwing and handling exceptions has always a performance penalty. How could we check if the group exists before trying to get/delete/add it from/to the collection?

After a short search on the .NET based samples I found:

  • A generic- and lambda expression-based solution, that is nice, but not easy to transfer to PowerShell.
  • An interesting solution, that uses the Xml property of the SPGroupCollection object .
  • A solution that is based on the GetCollection method of the SPGroupCollection object.

I chose the third sample to transfer to PowerShell. The equivalent PowerShell condition to check the group by ID:

@($web.SiteGroups.GetCollection(@(12345))).Count -eq 1

To check the group by name, we can use:

@($web.SiteGroups.GetCollection(@("YourGroupName"))).Count -eq 1

The parameter of the GetCollection method is an array, so we can use the same method to check if all or any of multiple groups exists.

For example, to check by ID if both of the groups we need exist:

@($web.SiteGroups.GetCollection(@(12345, 54321))).Count -eq 2

To check by name if any of the groups we need exists:

@($web.SiteGroups.GetCollection(@("YourGroupName1", "YourGroupName2"))).Count –gt 0

Older Posts »

The Shocking Blue Green Theme. Create a free website or blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

Join 53 other followers