Friday, September 16, 2016

Copy publishing page with web parts in Sharepoint Online via PowerShell

As you probably know for copying publishing pages with all web parts in Sharepoint we may use Sharepoint Designer. It is convenient way when you need to copy one page, but what if there are a lot of pages and copying them manually will take a lot of time? In this case we may use PowerShell. The tricky moment here is that when you copy page in Sharepoint Designer it uses author.dll RPC call (you may check it Fiddler). So we will do the same: construct http request with necessary parameters and authenticate ourselves using Sharepoint Online cookies. In our example we will copy standard search results page results.aspx on search sub to the same Pages doclib with different name:

   1: function Get-Authentication-Cookie($context, $siteCollectionUrl)
   2: {
   3:     $sharePointUri = New-Object System.Uri($siteCollectionUrl)
   4:     $authCookie = $context.Credentials.GetAuthenticationCookie($sharePointUri)
   5:     if( $? -eq $false )
   6:     {
   7:         return $null
   8:     }
   9:     $fedAuthString = $authCookie.TrimStart("SPOIDCRL=".ToCharArray())
  10:     $cookieContainer = New-Object System.Net.CookieContainer
  11:     $cookieContainer.Add($sharePointUri,
  12:     (New-Object System.Net.Cookie("SPOIDCRL", $fedAuthString)))
  13:     return $cookieContainer
  14: }
  15:  
  16: function Copy-Page($ctx, $site, $web, $serviceName, $sourcePage, $destPage) 
  17: { 
  18:     Write-Host "Copy page $sourcePage to $destPage on" $web.Url
  19: -foregroundcolor green
  20:  
  21:     $requestUrl = $web.Url + "/_vti_bin/_vti_aut/author.dll"
  22:     $request = $ctx.WebRequestExecutorFactory.
  23: CreateWebRequestExecutor($ctx, $requestUrl).WebRequest
  24:     $request.Method = "POST"
  25:     $request.Headers.Add("Content", "application/x-vermeer-urlencoded")
  26:     $request.Headers.Add("X-Vermeer-Content-Type",
  27: "application/x-vermeer-urlencoded")
  28:     $request.UserAgent = "FrontPage"
  29:     $request.UseDefaultCredentials = $false
  30:     
  31:     $cookiesContainer = Get-Authentication-Cookie $ctx $site.Url
  32:     $request.CookieContainer = $cookiesContainer
  33:     #$request.ContentLength = 0
  34:     
  35:     $rpcCallString =
  36:         "method=move+document%3a15%2e0%2e0%2e4569&service%5fname=" +
  37:         "$([System.Web.HttpUtility]::UrlEncode($serviceName))&oldUrl=" +
  38:         "$([System.Web.HttpUtility]::UrlEncode($sourcePage))&newUrl=" +
  39:         "$([System.Web.HttpUtility]::UrlEncode($destPage))&url%5flist=" +
  40:         "%5b%5d&rename%5foption=nochangeall&put%5foption=edit&docopy=true"
  41:  
  42:     $requestStream = $request.GetRequestStream()
  43:     $rpcHeader = [System.Text.Encoding]::UTF8.GetBytes($rpcCallString)
  44:     $requestStream.Write($rpcHeader, 0, $rpcHeader.Length);
  45:     $requestStream.Close();
  46:     
  47:     #$request.ContentLength = 0
  48:      
  49:     $response = $request.GetResponse()
  50:     $stream = $response.GetResponseStream()
  51:          
  52:     $reader = New-Object System.IO.StreamReader($stream)
  53:     $content = $reader.ReadToEnd()
  54:     $reader.Close()
  55:     $reader.Dispose()
  56:     
  57:     Write-Host "Page is successfully copied" -foregroundcolor green 
  58: }
  59:  
  60: function Add-New-Search-Results-Page($ctx, $site, $webRelativeUrl,
  61: $sourcePageName, $targetPageName)
  62: {
  63:     Write-Host "Process site $webRelativeUrl" -foregroundcolor green
  64:     
  65:     $web = $site.OpenWeb($webRelativeUrl)
  66:     $ctx.Load($web)
  67:     $ctx.ExecuteQuery()
  68:  
  69:     Write-Host "Server relative url" $web.ServerRelativeUrl -foregroundcolor green
  70:     
  71:     $lists = $web.Lists
  72:     $ctx.Load($lists)
  73:     $ctx.ExecuteQuery()
  74:     $pagesList = $null
  75:     foreach($l in $lists)
  76:     {
  77:         Load-CSOMProperties -object $l -propertyNames @("Title", "BaseTemplate")
  78: -executeQuery
  79:         #Write-Host $l.Title $l.BaseTemplate
  80:         if ($l.BaseTemplate -eq 850)
  81:         {
  82:             $pagesList = $l
  83:             break
  84:         }
  85:     }
  86:     
  87:     if ($pagesList -eq $null)
  88:     {
  89:         Write-Host "Pages doclib not found" -foregroundcolor red
  90:         return
  91:     }
  92:     
  93:     $pages = $pagesList.GetItems(
  94: [Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery())
  95:     $ctx.Load($pages)
  96:     $ctx.ExecuteQuery()
  97:     $sourcePage = $null
  98:     foreach($p in $pages)
  99:     {
 100:         Load-CSOMProperties -object $p -propertyNames @("File") -executeQuery
 101:         Write-Host $p.File.ServerRelativeUrl
 102:         
 103:         if ($p.File.ServerRelativeUrl.ToLower().EndsWith("/" +
 104: $sourcePageName.ToLower()))
 105:         {
 106:             Write-Host "Source page found" -foregroundcolor green
 107:             $sourcePage = $p.File.ServerRelativeUrl
 108:             continue
 109:         }
 110:         if ($p.File.ServerRelativeUrl.ToLower().EndsWith("/" +
 111: $targetPageName.ToLower()))
 112:         {
 113:             Write-Host "Page $targetPageName already exists"
 114: -foregroundcolor yellow
 115:             return
 116:         }
 117:     }
 118:     
 119:     if ($sourcePage -eq $null)
 120:     {
 121:         Write-Host "Source page not found" -foregroundcolor red
 122:         return
 123:     }
 124:  
 125:     $serviceName = $webRelativeUrl.SubString($webRelativeUrl.LastIndexOf("/"))
 126:     $sourcePage = $sourcePage.SubString($web.ServerRelativeUrl.Length + 1)
 127:     $targetPage =
 128: $sourcePage.SubString(0, $sourcePage.IndexOf($sourcePageName)) + $targetPageName
 129:  
 130:     Write-Host "Service name $serviceName" -foregroundcolor green
 131:     Write-Host "Source page $sourcePage" -foregroundcolor green
 132:     Write-Host "Target page $targetPage" -foregroundcolor green
 133:     
 134:     Copy-Page $ctx $site $web $serviceName $sourcePage $targetPage
 135: }
 136:  
 137: $siteUrl = "https://mytenant.sharepoint.com/sites/foo"
 138: $username = Read-Host -Prompt "Enter username"
 139: $password = Read-Host -Prompt "Enter password"
 140:  
 141: $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl)    
 142: $ctx.RequestTimeOut = 1000 * 60 * 10;
 143: $ctx.AuthenticationMode =
 144: [Microsoft.SharePoint.Client.ClientAuthenticationMode]::Default
 145: $securePassword = ConvertTo-SecureString $password -AsPlainText -Force
 146: $credentials = New-Object Microsoft.SharePoint.Client.
 147: SharePointOnlineCredentials($username, $securePassword)
 148: $ctx.Credentials = $credentials
 149: $web = $ctx.Web
 150: $site = $ctx.Site
 151: $ctx.Load($web)
 152: $ctx.Load($site)
 153: $ctx.ExecuteQuery()
 154:  
 155: Add-New-Search-Results-Page $ctx $site "en/search" "results.aspx" "fooresults.aspx"

Here we used additional helper function Load-CSOMProperties from Gary Lapointe which allows to load object with specified properties in PowerShell (in C# we may use ClientContext.Load() for that with properties defined via lambda expressions). I won’t copy it here, you may just copy it from github to your ps1 file as is.

Lets check what script does. At first we specify username and password and initialize ClientContext (lines 137-153). The we pass context and site objects to Add-New-Search-Results-Page, also pass sub site url “en/search”, source page “results.aspx” and target page “fooresults.aspx” (line 155). Inside function we initialize sub site and get Pages doclib (lines 63-91). After that we iterate through all pages and find specified source page and at the same time check that target page doesn’t exist yet (lines 93-132). Having source page url and target page urls in form “Pages/results.aspx” and “Pages/fooresults.aspx” and additional parameter called service name which is needed for RPC post value in form “/search” we make call to Copy-Page function which makes actual copy (lines 130-134). In Copy-Page we construct http request object (lines 21-33). In order to authenticate http request we set HttpWebRequest.CookieContainer property using another helper function Get-Authentication-Cookie (lines 31-32). Then we construct RPC call post parameter (lines 35-40) and perform http request (42-55). In result we will have new fooresults.aspx page with the same web parts as original result.aspx page. Hope this information will help you in your work.

Wednesday, August 31, 2016

Get title of the current list in Sharepoint via javascript object model

In basic Sharepoint object model we can get title of current list (list which is currently opened in browser) by calling SPContext.Current.List.Title. However in Sharepoint Online we need to use javascript object model where the same task is done little bit more tricky. Here is the code:

   1: SP.SOD.executeFunc("sp.js", "SP.ClientContext", function () {
   2:     SP.SOD.executeFunc("sp.core.js", "SP.ListOperation.Selection",
   3:         function () {
   4:         var listId = SP.ListOperation.Selection.getSelectedList();
   5:         if (typeof (listId) == "undefined" || listId == null || listId == "") {
   6:             return;
   7:         }
   8:         var ctx = SP.ClientContext.get_current();
   9:         var list = ctx.get_web().get_lists().getById(listId);
  10:         ctx.load(list);
  11:         ctx.executeQueryAsync(
  12:             Function.createDelegate(this, function (sender, args) {
  13:                 console.log(list.get_title());
  14:             }),
  15:             Function.createDelegate(this, function (sender, args) {
  16:                 console.log(args.get_message());
  17:             })
  18:         );
  19:     });
  20: });

I.e. at first we get id of the current list by calling SP.ListOperation.Selection.getSelectedList() (line 4) and then get list instance by this id and load it from context (lines 8-18). In success handler list.get_title() is available.

Disable Sync link and ribbon button for document libraries in Sharepoint via javascript

Offline sync of document libraries is new feature of Sharepoint 2013 which allows users to sync documents from doclibs on their PC:

There are 2 links: in QCB (quick control block) and in ribbon. In some cases organizations want to disable this functionality. From UI it can be done by going to Site settings > Search and Offilne availability and setting Offline Client availability > Allow items from this site to be downloaded to offilne clients to No:

If we want to do it programmatically we need to set SPWeb.ExcludeFromOfflineClient property to true via basic Sharepoint object model. However if we will need to do that for Sharepoint Online, we will face with the issue, because currently client-side object model doesn’t support it (see web.ExcludeFromOfflineClient property with CMOS discussion). So at least for now we have to do that via javascript.

The tricky part here is that ribbon is not loaded automatically when page is loaded, i.e. appropriate elements won’t be available in DOM. But we may use workaround: create function which will call itself after some time interval (e.g. each 2 seconds) until it will find Sync button which means that user opens the ribbon. Here is the code:

   1: disableSynButtonInDocLibs: function () {
   2:     // qcb (above the list view)
   3:     var qcbButton = $("button[class*='js-listview-qcbSyncButton']");
   4:     if (!qcbButton.hasClass("ms-disabled")) {
   5:         qcbButton.attr("disabled", true).addClass("ms-disabled");
   6:         qcbButton.children().each(function () {
   7:             $(this).addClass("ms-disabled");
   8:         });
   9:     }
  10:  
  11:     // ribbon Sync button
  12:     var syncLink = $("a[id='Ribbon.Library.Actions.Sync-Large']");
  13:     if (syncLink.length == 0) {
  14:         // ribbon is not loaded yet
  15:         setTimeout(disableSynButtonInDocLibs, 2000);
  16:     }
  17:  
  18:     syncLink.each(function () {
  19:         var this_ = $(this);
  20:         if (this_.hasClass("ms-cui-disabled")) {
  21:             return;
  22:         }
  23:         this_.attr("unselectable", "on");
  24:         this_.attr("aria-disabled", "true");
  25:         this_.addClass("ms-cui-disabled", "true");
  26:         this_.click(function (e) {
  27:             e.stopPropagation();
  28:         });
  29:         this_.children().each(function () {
  30:             $(this).click(function (e) {
  31:                 e.stopPropagation();
  32:             });
  33:         });
  34:     });
  35: }

On lines 2-9 we disable QCB Sync link, and on lines 11-34 – Sync ribbon button. On lines 12-16 we check is the button already loaded and if not call itself after 2 seconds. Note how we disable onclick event for the ribbon button: lines 26-33. In order to do that we attach new event handler to the link, which represents ribbon button, and its child elements and stop event propagation to prevent default event handler to be triggered.