Display Site Classification in SharePoint Sites

Today I was thinking about a simple solution to display a classification on a SharePoint site. There is a solution available in MSDN, but for me it was to complex. My solution uses some JavaScript injection that was taken from an PnP sample. This sample in combination with a property in the root folder of a library would be an easy solution.

The JavaScript file would look like this (in my case I called this file ShowSiteClassification.js.

var jQuery = "https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.2.min.js";

// Is MDS enabled?
if ("undefined" != typeof g_MinimalDownload && g_MinimalDownload && (window.location.pathname.toLowerCase()).endsWith("/_layouts/15/start.aspx") && "undefined" != typeof asyncDeltaManager) {
    // Register script for MDS if possible
    RegisterModuleInit("ShowSiteClassification.js", JavaScript_Embed); //MDS registration
    JavaScript_Embed(); //non MDS run
} else {
    JavaScript_Embed();
}

function JavaScript_Embed() {

    loadScript(jQuery, function () {
        $(document).ready(function () {
            // Execute status setter only after SP.JS has been loaded
            SP.SOD.executeOrDelayUntilScriptLoaded(function () { GetSiteClassification(); }, 'sp.js');
        });
    });
}

function GetSiteClassification() {
    var ctx = new SP.ClientContext.get_current();

    // var webProperties = ctx.get_web().get_allProperties();
    var webProperties = ctx.get_site().get_rootWeb().get_lists().getByTitle("Scripts").get_rootFolder().get_properties();
    ctx.load(webProperties);
    ctx.executeQueryAsync(Function.createDelegate(this, GetSiteClassificationSuccess), Function.createDelegate(this, GetSiteClassificationFailure));

    function GetSiteClassificationSuccess() {
        var allProps = webProperties.get_fieldValues();

        var customProp = "";

        //make sure the property is there before using it.
        if (webProperties.get_fieldValues().SiteClassification != undefined) {
            var customProp = webProperties.get_fieldValues().SiteClassification;
            var message = "The classification of this site is: " + customProp;

            switch (customProp) {
                case "Internal":
                    var strStatusID = SP.UI.Status.addStatus("Information : ", message, true);
                    SP.UI.Status.setStatusPriColor(strStatusID, "green");
                    break;

                case "Confidential":
                    var strStatusID = SP.UI.Status.addStatus("Information : ", message, true);
                    SP.UI.Status.setStatusPriColor(strStatusID, "yellow");
                    break;

                case "Secret":
                    var strStatusID = SP.UI.Status.addStatus("Information : ", message, true);
                    SP.UI.Status.setStatusPriColor(strStatusID, "red");
                    break;

                default:
                    break;
            }
        }
    }

    function GetSiteClassificationFailure() {
        var message = "There was a failure getting the web properties.";
        var strStatusID = SP.UI.Status.addStatus("Error : ", message, true);
        SP.UI.Status.setStatusPriColor(strStatusID, "red");
    }
}

function loadScript(url, callback) {
    var head = document.getElementsByTagName("head")[0];
    var script = document.createElement("script");
    script.src = url;

    // Attach handlers for all browsers
    var done = false;
    script.onload = script.onreadystatechange = function () {
        if (!done && (!this.readyState
					|| this.readyState == "loaded"
					|| this.readyState == "complete")) {
            done = true;

            // Continue your code
            callback();

            // Handle memory leak in IE
            script.onload = script.onreadystatechange = null;
            head.removeChild(script);
        }
    };

    head.appendChild(script);
}

To store this file, I created a new library in the root web of my site collection. In this library, I broke the permission inheritance and set read permissions for Everyone except External Users (could also be just Everyone). Then I added the script to the custom actions of the site:

Add-PnPJavaScriptLink -Name ShowSiteClassification -Url https://mytenant.sharepoint.com/sites/site-class/Scripts/ShowSiteClassification.js -Scope Site

Finally, I set the SiteClassification property in the root folder of the library I created.

Set-PnPPropertyBagValue -Key "SiteClassification" -Value "Confidential" -Folder /Scripts

With this solution, the classification is also visible in subwebs, even when the user does not have permissions to the root web.

Title Field in JSLink Scripts

Today I had the need to modify the output of the title field in a SharePoint list view. It’s a perfect task for JSLink. So, created the script, added the file to my SharePoint site and configured the view to use this JSLink file. A straight forward process.

But I was wondering, why my output was not modified. I always saw the value, as it would come without a JSLink modification. Did some debugging and my function was never called.

Thought a little about the title field, and stop. In views, we have three different options to display the title and in view definitions we use separate field names (fieldref).

  • Title (Title only, no link)
  • LinkTitle (Title with link and with “…” suffix to open a dropdownmenu)
  • LinkTitleNoMenu (Title with link to the document or file but no “…” suffix)

Because in my view I used the LinkTitle, but mapped my function in the JSLink to the Title field, it was clear that my function could not be called. Modified the JSLink file to use LinkTitle and everything worked as expected.

For the “lessons learned”: when creating a JSLink script for the Title field, consider all three field names.

Sending Mails with Utility.SendEmail() in SharePoint Online

In the client object model for SharePoint Online we have the Utility class. This class provides a function SendEmail() that (oh wonder) helps us to send emails. In my tests, I found that the recipient must be a member of the hidden “All People” group in the SharePoint site, from where we take the client context. So, to successfully send an email with this function, the code should look like this:


string userMail = "alexw@anytenant.onmicrosoft.com";

Web web = ctx.Web;
web.EnsureUser(userMail);
ctx.ExecuteQueryRetry();

EmailProperties mailProps = new EmailProperties();
mailProps.From = "noreply@anytenant.onmicrosoft.com";
mailProps.To = new string[] { userMail };
mailProps.Subject = "Email Tester";
mailProps.Body = @"<html><body><h1 style='color: red;'>Header</h1><br/><div>Hello world.</div></body></html>";

Utility.SendEmail(ctx, mailProps);
ctx.ExecuteQueryRetry();

Very important for the recipients: they must be a user in the current tenant. In my tests, it was not possible to send emails to external users, even when they have permissions in the SharePoint site of the client context.

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.