Update Theme on all Webs of Office365

So I have been doing a small migration project for a customer moving to Office 365. Finally I have a reason to bother working on Office 365. I have to say upfront: this is not a finished platform (yet!). Great potential, though.

As usual the initial idea is something completely different than what I spent most of my time on up to now.

Care to venture a guess?…of course: design. As usual theming is a b**** because you cannot deploy a solution on Office 365.

First I tried all the usual stuff: UI, but there are about 30-40 Sites (Subsites).
CSOM/ Webservices (CSOM is using the api webservices, if I am correct…).
Too bad the SPWeb.ApplyTheme Method doesn’t work as intended. Funny how Microsoft is, the method has four parameters. If you give it 3 it will tell you: you gave me only 3, I need 4, even though Technet tells you it’s fine to pass $null values, but that’s no biggy, because you could use dummy-data, right? So if you do that and you pass 4 values you get: “Too many resources requested” or a similar message translated from German (customer wants it in 1031 – good, that Office 365 has all the lang packs).

So the result I created is a PowerShell Script combining the chocolatey goodness of SPO Management PowerShell (get all the SPO-Site Objects of your tenant) with the caramely filling of SCOM (you cannot get any Web Objects, so we use SCOM for that)…and to top it off we use sweet-old Internet Explorer as a COM Application to fill out the form for applying the theme for each of the webs while iterating.

I would have liked to do it differently. In the traditional On-Premise Shell I could have used a one-line script to get this done. I could have guessed it in the beginning, that it would be a bit more difficult, but three different paradigms to get a simple thing done like theming-automation is a bit hilarious. That doesn’t compare to anything – at all!

I would have been fine with combining Office 365 Management Shell with SCOM. Okay – I mean the Management Shell includes only about 30 Commandlets at this point. It’s pretty much only good for creating Site Collections, emptying the recycle bin (which cannot be done at the moment via UI as I write this post – meeh) and adding users to SharePoint groups.

So we all know how bad this solution is – I am not even going to try to sell this to you as a good idea. It doesn’t work reliably. But it does reduce your workload. So that’s definitely sensible. I am imagining my customer telling me: “That theme is not good enough. Let’s use a different one.” This is probably what will happen. It usually does.

So it took me about the same time to complete the script as I would have to go through all of these webs and done all of the changes manually. So I have already won. Maybe I can help somebody else this way as well, so I am sharing the script here.

Keep in mind that you need to use the admin url to connect to the SPO-Service. Check this article on how to set up your environment accordingly. Set up the SharePoint Online Management Shell Windows PowerShell environment.

You should be aware, that if you wanna do this on a SharePoint 2010 machine you will have to open your powershell or your ISE with the suffix “-v2” like “%windir%\system32\WindowsPowerShell\v1.0\PowerShell_ISE.exe” because SharePoint 2010 will not work with .NET 4.0 when you install the management shell.

So the script needs the following:
– you need a palette and you can create that using an existing palette and edit it with the
– you need the management shell for Office 365 installed
– you need an Office 365 tenant and the credentials to access (duuuh!)

The script does the following:
– iterate over all sites that are not search or mysite (use oslo.master) or the public site
– when you get the rootweb from the site url: this is where you upload the palette (theme)
– get all the webs and iterate over them
– for the next step you will need to have an ie opened and authenticated against your tenant
– apply the theme via the two forms (you may think that because the second form has its own url you can skip the first one, but this one actually creates a cache version of the theme, so you will need to fill out the first form to be able to fill out the second one successfully).
– you will also say: why does he need the sleep commands? any good script will work without, but this is not one of those. We actually have to wait for the requests to be responded. It may even take a couple of seconds more than I have in my script. The anchors I use for clicking are not going to be found from the getelementby* methods if there isnÄt enough time in between.

So here is the “masterpiece”. Drop me a comment if it does in fact help you. Apart from that it may be a stepping stone for a much nicer script in the future.

param (
[string] $LocalPalettePath = “C:\Backup\my-palette.spcolor”,
[string] $username = “myusername@mytenant”,
[string] $password = “mypassword”,
[string] $url = “mytenant-admin.url”
)

function Process-File($Context, $File, $RemoteFolder) {
Write-Output (“Uploading ” + $File.FullName);
$FileStream = New-Object IO.FileStream($File.FullName,[System.IO.FileMode]::Open)
$FileCreationInfo = New-Object Microsoft.SharePoint.Client.FileCreationInformation
$FileCreationInfo.Overwrite = $true
$FileCreationInfo.ContentStream = $FileStream
$FileCreationInfo.URL = $File.Name
$Upload = $RemoteFolder.Files.Add($FileCreationInfo)
$Context.Load($Upload)
$Context.ExecuteQuery()
}

function Upload-Palette($ctx, $web, [string] $localPalettePath) {
$ctx.Load($web);
$ctx.ExecuteQuery();

#Write-Host “Web URL is” $web.Url
Write-Host($localPalettePath)
if($web.ServerRelativeUrl -ne “/”) {
$remoteFolderRelativeUrl = $web.serverRelativeUrl + “/_catalogs/theme/15/”;
} else {
$remoteFolderRelativeUrl = “/_catalogs/theme/15/”;
}
$remoteFolder = $web.getFolderByServerRelativeUrl($remoteFolderRelativeUrl);
$ctx.Load($remoteFolder)
$ctx.ExecuteQuery();

$file = get-item $localPalettePath;
Process-File $ctx $File $RemoteFolder;
}

function CreateTheme($ie, $ctx, $web, [string] $localPalettePath, [string] $rootWebUrl, [string] $rootWebRelativeUrl) {
$ctx.Load($web);
$ctx.ExecuteQuery();

$baseUrl = $web.Url.Replace($web.ServerRelativeUrl, “”);
$relativeWebUrl = $web.ServerRelativeUrl;

$localPaletteItem = get-item $localPalettePath
$localPaletteName = $localPaletteItem.Name;

$url = $web.Url;

if($relativeWebUrl -eq “/”) {
$relativeBase = “”;
} else {
$relativeBase = $relativeWebUrl;
}
if($rootWebRelativeUrl -eq “/”) {
$relativeBaseRoot = “”;
} else {
$relativeBaseRoot = $rootWebRelativeUrl;
}

$gallery = “/_layouts/15/start.aspx#/_layouts/15/designbuilder.aspx?masterUrl={0}&themeUrl={1}&imageUrl={2}&fontSchemeUrl={3}”;

$masterUrl = $relativeBase + “/_catalogs/masterpage/seattle.master”;
$themeUrl = $relativeBaseRoot + “/_catalogs/theme/15/$localPaletteName”;
$fontUrl = $relativeBaseRoot + “/_catalogs/theme/15/SharePointPersonality.spfont”;
$imageUrl = “”;

$masterUrlEncoded = [System.Web.HttpUtility]::UrlEncode($masterUrl);
$themeUrlEncoded = [System.Web.HttpUtility]::UrlEncode($themeUrl);
$fontUrlEncoded = [System.Web.HttpUtility]::UrlEncode($fontUrl);
$imageUrlEncoded = [System.Web.HttpUtility]::UrlEncode($imageUrl);

$formUrl = $url + [string]::Format($gallery, $masterUrlEncoded, $themeUrlEncoded, $imageUrlEncoded, $fontUrlEncoded);
$ie.Navigate($formUrl);
$ie.Visible = $true;
sleep 5;

“First Form:”
$formUrl

$ieDoc = $ie.Document;

$div = $ieDoc.getElementById(“ms-designbuilder-main”)

$anchor = $div.GetElementsByTagName(“a”) | ? { $_.id -and $_.id.endswith(“btnLivePreview”) }

$anchor.Click();

sleep 2;

“Second Form:”
$ie.LocationURL

$ieDoc = $ie.Document;

sleep 15;
$anchor = $ieDoc.GetElementById(“btnOk”);

$anchor.Click();

sleep 2;

“PostLocation:”
$ie.LocationUrl;

sleep 1;
}

# Control #

$cred = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist $userName, $(convertto-securestring $password -asplaintext -force)
$credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($username, $(convertto-securestring $password -asplaintext -force));

Connect-SPOService -Url $url -Credential $cred

$sposites = get-sposite | ? { -not $_.Url.Endswith(“search”) -and -not $_.Url.Contains(“-public.”) -and -not $_.Url.Contains(“-my.”) }

clear;

foreach($sposite in $sposites) {
if($sposite) {
$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($sposite.Url);
$ctx.Credentials = $credentials;

$rootWeb = $ctx.Web
$childWebs = $rootWeb.Webs
$ctx.Load($rootWeb);
$ctx.ExecuteQuery();

Write-Host $rootWeb.Url;

Upload-Palette $ctx $rootWeb $localPalettePath

$app = new-object -com shell.application
$ie = $app.windows() | ? { $_.Name -eq “Internet Explorer” } | select -first 1;

CreateTheme $ie $ctx $rootWeb $localPalettePath $rootWeb.Url $rootWeb.ServerRelativeUrl;

$ctx.Load($childWebs)
$ctx.ExecuteQuery()

foreach ($childWeb in $childWebs)
{
CreateTheme $ie $ctx $childWeb $localPalettePath $rootWeb.Url $rootWeb.ServerRelativeUrl;
}
}
}

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: