Apply site policy automatically on site creation in SharePoint Online

For a site lifecycle scenario we need to set a site collection to read-only. In the (good) old world with SharePoint on-premise that was a simple task. Because we are working with Office 365 and SharePoint Online, it is a little bit difficult, because we want a very high grade on automation.

First of all, there is no PowerShell cmdlet or anything in the client object model that would enable us to directly set a site collection to read-only. But, we have something in SharePoint that is called Site Policy. Such a policy applied to the root web of a site collection could set the state of the site collection to read-only, when the site is closed. Could be a simple workaround, but creating a site policy by code does not seem to be possible in SharePoint Online. But on the other hand, applying a site policy to a site could be done with classes and methods from the client object model. Does that make sense? Not for me.

Looking behind the scenes of a site policy (and reading other articles from Microsoft), a site policy is nothing else than a content type that is created in the web. First idea was to create this content type by code. The client object model has classes and methods to do that, but the configuration of the site policy is stored in the XmlDocument member of the content type and this member could not be set using CSOM or the Rest endpoint. Thanks a lot for making this happen.

Ok, saying goodbye from creating the site policy by code, we still need to create the site policy (or its content type) automatically, when the site collection is created. Publishing content types is one of the main tasks of the Content Type Hub in SharePoint. Having SharePoint Online, the Content Type Hub is automatically made available, when the tenant is created at https://tenantname.sharepoint.com/sites/contenttypehub. So, let’s create the site policy in the site collection of the Content Type Hub.

This will create the content type “Mark Readonly Tester” in this site collection. The content type is hidden by default, so we are not able to do anything via the web ui.

Next create a new site collection in the tenant. Could be a simple Team Site. When this site collection is created, switch to Site Settings and the Site Policy page (found in Site Collection Administration). And what do we see? No site policy. Investigating with the SharePoint Client Browser (many thanks to Bram de Jager!), the content type of the site policy was not published to our new site collection. Perfect, next problem to solve.

I did some really dirty stuff with this content type, and I do not know, whether it is supported by Microsoft, but after making the changes with the following client side code (that makes use of the Office 365 PnP Core), the content type for the site policy was published to a newly created site collection.

Web web = ctx.Web;
ContentType ct = web.GetContentTypeByName(contentTypeName);
FieldCollection fields = ct.Fields;
FieldLinkCollection fieldLinks = ct.FieldLinks;

ctx.Load(web);
ctx.Load(ct);
ctx.Load(fields);
ctx.Load(fieldLinks);
ctx.ExecuteQueryRetry();

ct.Hidden = false;
ct.Group = contentTypeGroup;

foreach (Field field in fields)
{
    field.Hidden = false;
    field.Update();
}

foreach (FieldLink fieldLink in fieldLinks)
{
    fieldLink.Hidden = false;
}

ct.Update(true);

web.AddFieldToContentTypeByName(contentTypeName, new Guid("{9ebcd900-9d05-46c8-8f4d-e46e87328844}")); // field: Categories

web.Update();
ctx.ExecuteQueryRetry();

When a new site collection is created, we can see the site policy in the site settings of the site collection.

When we switch to the Site Closure and Delection page in the Site Administration of the site settings, we can select the site policy.

And when this policy is selected and the site will be closed, the read-only setting will do what it should.

The same result we can get by running this short piece of code that we will now can use in our site lifecycle scenario to set a site collection to read-only.

Web web = ctx.Web;
web.ApplySitePolicy("Mark Readonly Tester");
web.Update();
ctx.ExecuteQueryRetry();

ProjectPolicy.CloseProject(ctx, web);
ctx.ExecuteQueryRetry();

Once again, actually I do not know, whether this is a supported scenario or not, but it will solve the problem to deploy a site policy to a new site collection.

 

SharePoint – Ribbon-Scripts with external JavaScript-files

Customizing the SharePoint ribbon is not that easy it could be, when we need to package our customizations for deployments in any environment. But with the Office 365 PnP PowerShell extensions we have tools to make life a litte easier.

What I am struggling with often, is debugging the code for the CommandAction or the EnabledScript attributes in a CommandUIHandler. But there is an easy technique to extract the script from the ribbon definition (the CommandUIExtension). In this example a simple button should be added to the ribbon for elements of the content type “Document” (0x0101). When the user clicks the button in the ribbon we just show a simple dialog with the ID of the document.

The xml for the button is as follows:

<CommandUIExtension>
	<CommandUIDefinitions>
		<CommandUIDefinition Location="Ribbon.Library.ViewFormat.Controls._children">
			<Button Id="Ribbon.Library.ViewFormat.About"
				Command="AboutButtonCommand"
				LabelText="About"
                Image32by32="{SiteUrl}/_layouts/15/1033/Images/formatmap32x32.png?rev=23"
                Image32by32Top="-273"
                Image32by32Left="-1"
                Description="About"
                TemplateAlias="o1" />
		</CommandUIDefinition>
	</CommandUIDefinitions>
	<CommandUIHandlers>
		<CommandUIHandler
			Command="AboutButtonCommand"
			CommandAction="javascript:aboutScript({SelectedItemId});"
			EnabledScript="javascript:onlyOneItemSelected();" />
	</CommandUIHandlers>
</CommandUIExtension>

As we see in the CommandUIHandler, the CommandAction and the EnabledScript just contain calls to a JavaScript-function. These functions are placed in a simple JavaScript file:


function aboutScript(itemId) {
	alert("Hello user! You have selected item " + itemId);
}

function onlyOneItemSelected() {
	return (SP.ListOperation.Selection.getSelectedItems().length == 1)
}

To make this code available, we will store the file with the JavaScript-code in a document library in our SharePoint site. In the example, I prepared a library “Scripts” to store the file.

Now we can store the script file in the library, add the extension to the ribbon and make the file available for the ribbon extension. This could be done with the Office 365 PnP PowerShell extensions:

Add-PnPFile -Path .\AboutButtonScript.js -Folder "Scripts"

$ribbon = Get-Content .\MyRibbon.xml
$ribbon = [string]$ribbon
Add-PnPCustomAction -Name "RibbonTester" -Title "RibbonTester" -Description "-" -Group "Tester" -Location "CommandUI.Ribbon" -CommandUIExtension $ribbon -RegistrationType ContentType -RegistrationId 0x0101

Add-PnPJavaScriptLink -Name "AboutButtonScript" -Url https://mytenant.sharepoint.com/sites/perm-tester/Scripts/AboutButtonScript.js -Scope Web

The result will look like this:

When we select a document in the library and click the button, the dialog will show the ID of the selected document.

Important: In our JavaScript file we cannot use the substation tokens, as it is shown in the CommandUIHandler Element description in MSDN. But as we have seen in the MyRibbon.xml example, we can use these tokens as parameters.

With the JavaScript code outside the CommandUIExtension, it is now very easy to develop and debug the code, because all we need to do is working the the script file that is stored in the library. Another advantage of this approach: we can reuse code for several implementations.

AppRegNew.aspx in PowerShell

To add and provision a provider hosted app in SharePoint Online it is necessary to register the app using “…/_layouts/15/appregnew.aspx”.

I was wondering, why there isn’t a PowerShell cmdlet to do the same thing to automate the deployment process. But often life is so easy. When using AppRegNew.aspx in SharePoint Online, a new service principal is created in Azure Active Directory. So, all we need to use PowerShell to register our app is the Microsoft Azure Active Directory PowerShell Module. With this module we can use a simple PowerShell script for the registration:

$ClientId = "add your client id here" 
$ClientSecret = "add your client secret here" 
$Title = "DemoProviderHostedApp" 
$AppDomain = "whatever-you-want-here.azurewebsites.net" 
$RedirectURI = "https://whatever-you-want-here.azurewebsites.net/demoproviderhostedapp/pages/default.aspx" 

$appPrincipalId = $ClientId 
$displayName = $Title 
$servicePrincipalNames = @($ClientId, "$ClientId/$AppDomain") 
$addresses = New-MsolServicePrincipalAddresses -Address $RedirectURI -AddressType Reply 

New-MsolServicePrincipal -ServicePrincipalNames $servicePrincipalNames -AppPrincipalId $appPrincipalId -DisplayName $displayName -AccountEnabled $true -Addresses $addresses -Type Password -Value $ClientSecret 

New-MsolServicePrincipalCredential -AppPrincipalId $appPrincipalId -Type Symmetric -Usage Sign -Value $ClientSecret 
New-MsolServicePrincipalCredential -AppPrincipalId $appPrincipalId -Type Symmetric -Usage Verify -Value $ClientSecret 

Important: you need to be an administrator in the Azure tenant to run the PowerShell commands.

To check, whether your application is already registered or not, you can use this script:

Get-MsolServicePrincipal -AppPrincipalId {add your client id here}

If your app is not already registered, you will get an error message. Otherwise information about your app is shown.

SharePoint Online – Working with Search Settings with Office 365 PnP PowerShell

With the PnP PowerShell extensions we have a lot of cmdlets to customize our sites in SharePoint Online. There are also cmdlets to move the search configuration from a development environment to a test or production environment. That makes it simple to create new solutions in SharePoint Online. This walkthrough will show how we can configure the search configuration in our development environment and how to prepare a PowerShell script for deployment. You should be somewhat familiar with the configuration options via the web interface in SharePoint Online.

In this example we start with a simple team site. Using PowerShell we create a new library called “Library” (keep it simple) and in this library a new text field called “Freetext”. As we know in SharePoint, when we store documents in the library and store data in the field, the crawler will create a new crawled property. We will use this crawled property to create a new managed property. Additionally we will connect the crawled property to one of the managed properties prepared for the refinement. To restrict the search results to our site collection we will create a new result source. To test and use our search configuration we will create a subsite with the basic search center. In this search center we will modify the search results webpart and the refinement webpart. Finally we will create one single script to deploy our customizations to a new site collection. Let’s go.

A site collection we can use for our development steps is already created in SharePoint Online. First we will create our library and its field. This could be done with a simple PowerShell script (the first part of our deployment script):


$list = New-PnPList -Title "Library" -Template DocumentLibrary -Url "Library" -OnQuicklaunch

$field = Add-PnPField -List "Library" -DisplayName "Freetext" -InternalName "Freetext" -Type Text -AddToDefaultView

Next we need to upload a document to this new library and set the value of our field. This is necessary to create the crawled property in the search configuration. Now we need a little patience, because the crawler needs to crawl the content of our library. It might take 5-10min until the document and the metadata appears in the search results (you can test this using the search box in the top right of the site collection).

Now we can start modifying the search configuration. We will do this using the web interface and the site settings of our development site. We open the site settings and click the link “Search Schema” in the Site Collection Administration section. Next click the “New Managed Property” link. Enter a name for the property (do not use spaces or special characters in the name field), mark the property as

  • Searchable
  • Queryable
  • Retrievable

Scroll down to the section for the “Mappings to crawled properties” and click the “Add a Mapping” button. Search for the crawled property that matches to the field (in our case “ows_Freetext”) and add it to the mappings as we see in the following image.

Scroll down and click the OK button to create the managed property.

Now we need to configure the managed property for the refinements. The starting point is the search schema again. In the filter box for the managed properties search for “refinablestring”.

Click one of the properties found that does not have a mapped crawled property. This will open the settings for this managed property. Scroll down to the section for the “Mappings to crawled properties” and click the “Add a Mapping” button. Search for the crawled property that matches to the field (in our case “ows_Freetext”) and add it to the mappings. That were all settings we need to do in the search schema.

Next we will create a new result source that we use in the search results webpart to limit the search results to the current site collection. In the site settings of the site collection we click the link “Search Result Sources” in the Site Collection Administration section. Click the link “New Result Source”. Type a name for the result source (I used “Current Site Collection”) and scroll down to the Query Transform section. Click the button “Launch Query Builder”. In the Property Filter combobox select “–Show all managed properties–“. That is necessary, because we need to choose the property “SPSiteURL”. As the value select “This site collection”. The query should look like this, after you clicked “Add property filter”.

Now, after we have finalized our necessary search configuration, we can do the export of the configuration. To do this, we have two options:

  • Use Search Configuration Export from the site settings (Site Collection Administration section)
  • Use the Get-PnPSearchConfiguration cmdlet from the PnP PowerShell extensions and use the scope “Site”

Both will produce the same result and create an xml-file with the current search configuration of our site collection. This file will be used by our deployment script to set the search configuration in our target environment.

Next we need a subsite for our basic search center. To create this subsite I use a PowerShell script:


$SubSearchWebTemplate = "SRCHCENTERLITE#1"
$SubSearchDescription = "Search"
$SubSearchTitle = "Search"
$SubSearchUrl = "Search"
$SubSearchLanguage = "1033"
$searchWeb = New-PnPWeb -Title $SubSearchTitle -Url $SubSearchUrl -Description $SubSearchDescription -Locale $SubSearchLanguage -Template $SubSearchWebTemplate

When the subsite is created, open the subsite in a browser and run a search (we need to modify the results.aspx). On the results page (results.aspx) we need to modify the webpart for the refinement panel and the search results webpart.

In the webpart properties for the refinement webpart click the “Choose Refiners…” button and add the RefinableString managed property, we modified a few steps above. Don’t forget to set the display name for the refiner.

In the properties of the search results webpart, click the “Change query” button. In the query builder choose the result source we created in our site collection from the combobox near “Select a query”. That is all we need to configure for the search results webpart.

To make the modifications in the webparts available by our deployment script, we need to export the webpart settings. This could easily be done from the web interface.

Do this export for the refinement webpart and the search results webpart.

Now we have the following artefacts

  • Search-Config.xml
  • Refinement.webpart
  • SearchResults.webpart

So, let’s create the deployment script to make all the customizations in the target environment. The script should do the following:

1. Create the library and its field

2. Import the search configuration

3. Create the search subsite

4. Modifiy the refinement webpart and the search result webpart in the search subsite

5. Configure the search settings in the root site of the site collection (could be done by setting properties in the property bag of the web)

Finally the deployment script will look like this (yes, I know, do not use full qualified paths in these scripts, but it’s just a demo):

Param (
		[Parameter(Mandatory=$true)]
		[string] $Url
)

Function EstablishConnectionSilent
{
	Param (
		[Parameter(Mandatory=$true)]
		[string] $Url,
		$Credentials
	)
	
	if ($Credentials -eq $null)
	{
		$Credentials = Get-Credentials
	}
	
	Connect-PnPOnline -Url $Url -Credentials $Credentials -ErrorAction Stop
}

$cred = Get-Credential

Write-Host "Connect to site collection"
Connect-PnPOnline -Url $Url -Credentials $cred

Write-Host "Create library"
$list = New-PnPList -Title "Library" -Template DocumentLibrary -Url "Library" -OnQuicklaunch

Write-Host "Add field to library"
$field = Add-PnPField -List "Library" -DisplayName "Freetext" -InternalName "Freetext" -Type Text -AddToDefaultView

Write-Host "Import search configuration"
$sConfig = Set-PnPSearchConfiguration -Scope Site -Path C:\Scripts\Search-Demo\Search-Config.xml

Write-Host "Create search subweb"
$SubSearchWebTemplate = "SRCHCENTERLITE#1"
$SubSearchDescription = "Search"
$SubSearchTitle = "Search"
$SubSearchUrl = "Search"
$SubSearchLanguage = "1033"
$searchWeb = New-PnPWeb -Title $SubSearchTitle -Url $SubSearchUrl -Description $SubSearchDescription -Locale $SubSearchLanguage -Template $SubSearchWebTemplate

if ($searchWeb -ne $null)
{
	$searchWebUrl = $searchWeb.Url

	Write-Host "Switch context to search subweb"
	EstablishConnectionSilent -Url $searchWebUrl -Credentials $cred
	
	$resultsPage = $searchWeb.ServerRelativeUrl + "/results.aspx"

	Write-Host "Remove webparts"
	Remove-PnPWebPart -ServerRelativePageUrl $resultsPage -Title "Refinement"
	Remove-PnPWebPart -ServerRelativePageUrl $resultsPage -Title "Search Results"
	
	Write-Host "Add webparts"
	$wp = Add-PnPWebPartToWebPartPage -ServerRelativePageUrl $resultsPage -Path C:\Scripts\Search-Demo\Refinement.webpart -ZoneId "NavigationZone" -ZoneIndex 1
	$wp = Add-PnPWebPartToWebPartPage -ServerRelativePageUrl $resultsPage -Path C:\Scripts\Search-Demo\SearchResults.webpart -ZoneId "MainZone" -ZoneIndex 2

	Write-Host "Switch context to root web"
	EstablishConnectionSilent -Url $Url -Credentials $cred

	Write-Host "Set search settings in web property bag"
	Set-PnPPropertyBagValue -Key "SRCH_ENH_FTR_URL" -Value $searchWebUrl
	Set-PnPPropertyBagValue -Key "SRCH_ENH_FTR_URL_SITE" -Value $searchWebUrl
	$value = '{"Inherit":false,"ResultsPageAddress":"' + $resultsPage + '","ShowNavigation":false}'
	Set-PnPPropertyBagValue -Key "SRCH_SB_SET_SITE" -Value $value
}

Write-Host "Done."

To deploy our configuration to a new site collection, simply run the script with a proper url parameter:

Now we can upload new documents to our library, set the values for our Freetext field and wait for a crawl (remember, be patient). When the search index is updated, the search results should look similar to the next image.

Hope this will help in the next customizations, where modifications to the search configuration are necessary.

Hide Button in Ribbon with Office 365 PnP PowerShell

Sometimes it is necessary to hide a button in the ribbon of SharePoint. With the Office 365 PnP PowerShell extensions this task could be done very easy. In this example, we will hide the “Publish” button in the ribbon for a document library.

First, we need the ID/location of the element in the ribbon. This is the difficult part in the whole task. In most cases, we can find this element with the developer tools of the browser.

In our case the ID/location of the button is “Ribbon.Documents.Workflow.Publish”.

Next we need an xml file that will be used in the cmdlet from the PnP PowerShell extensions. In this simple xml file we need the ID/location, we found using the developer tools:

<CommandUIExtension>
    <CommandUIDefinitions>
        <CommandUIDefinition Location="Ribbon.Documents.Workflow.Publish" />
    </CommandUIDefinitions>
</CommandUIExtension> 

Last but not least, we can run the Set-SPOCustomAction cmdlet from the PnP PowerShell extensions:


$fileContent = Get-Content ".\DisablePublish.xml"
$fileContentAsString = [string]$fileContent

Add-SPOCustomAction -Name "DisablePublish" -Title "DisablePublish" -Description "-" -Group "a" -Location "CommandUI.Ribbon" -RegistrationType List -Sequence 10000 -RegistrationId 101 -CommandUIExtension $fileContentAsString

And that is the result:

To make the button visible again, use the cmdlet Remove-SPOCustomAction from the PnP PowerShell extensions. First, run Get-SPOCustomAction, to get the ID of the custom action, then run Remove-SPOCustomAction with this ID.

Add an assembly reference in an Azure Function

When we develop solutions in Visual Studio, we can add references to the assemblies we need in our code. When we create an Azure Function, we do not have this option in the user interface in our browser.

But adding a reference is easy. To add it, just write all reference names with a leading ‘#r’ on top of the csx-file, as shown in the following image.