I've moved my blog to!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Saturday, February 7, 2009

Setting the Master Page Using STSADM

I’d thought about building an STSADM command to enable setting the master page of a site for quite a while but had opted not to do it simply out of principle – it’s generally a better idea to do this via a Feature and I didn’t really want to promote a bad practice.  Ultimately though I had to concede that there are administrators who will not have the luxury of having developers who can create Feature that can be deployed to enable consistent application of their master page across site collections.

So what I came up with was a command, called gl-setmasterpage, which allows the user to set the site and system master page URLs and, this is the cool part, copy a master page from a source location to the destination site.  So consider that you have 10 different site collections on your main portal and you want all those site collections to use the same master page as the root site collection – you could accomplish this by running the following for each site collection (or by wrapping in a loop using PowerShell as shown further down):

stsadm -o gl-setmasterpage -url "http://portal/division1" -sitemaster "/division1/_catalogs/masterpage/custom.master" -systemmaster "/division1/_catalogs/masterpage/custom.master" –sitesource "http://portal/_catalogs/masterpage/custom.master"

The code to set the master page is pretty simple – there’s two core properties of the SPWeb object: CustomMasterUrl and MasterUrl.  The CustomMasterUrl property corresponds to the “Site Master Page” value when editing the settings via the browser and the MasterUrl property corresponds to the “System Master Page” value.  The bulk of the code for this command is in the validation and copying of the source master page:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Collections.Specialized;
   4: using System.IO;
   5: using System.Text;
   6: using Lapointe.SharePoint.STSADM.Commands.Lists;
   7: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
   8: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   9: using Microsoft.SharePoint;
  10: using Microsoft.SharePoint.Deployment;
  12: namespace Lapointe.SharePoint.STSADM.Commands.SiteCollectionSettings
  13: {
  14:     public class SetMasterPage : SPOperation
  15:     {
  16:         /// <summary>
  17:         /// Initializes a new instance of the <see cref="SetMasterPage"/> class.
  18:         /// </summary>
  19:         public SetMasterPage()
  20:         {
  21:             SPParamCollection parameters = new SPParamCollection();
  22:             parameters.Add(new SPParam("url", "url", true, null, new SPUrlValidator(), "Please specify the web url."));
  23:             parameters.Add(new SPParam("sitemaster", "sitemp", false, null, new SPNonEmptyValidator()));
  24:             parameters.Add(new SPParam("systemmaster", "sysmp", false, null, new SPNonEmptyValidator()));
  25:             parameters.Add(new SPParam("resetsubsites", "reset"));
  26:             parameters.Add(new SPParam("systemsource", "syssrc", false, null, new SPUrlValidator()));
  27:             parameters.Add(new SPParam("sitesource", "sitesrc", false, null, new SPUrlValidator()));
  29:             StringBuilder sb = new StringBuilder();
  30:             sb.Append("\r\n\r\nSets the site and/or system master page for the given web.\r\n\r\nParameters:");
  31:             sb.Append("\r\n\t-url <web URL>");
  32:             sb.Append("\r\n\t[-sitemaster <server relative URL to the site master page>]");
  33:             sb.Append("\r\n\t[-systemmaster <server relative URL to the system master page>]");
  34:             sb.Append("\r\n\t[-resetsubsites]");
  35:             sb.Append("\r\n\t[-systemsource <URL to a source system master page file to copy to the target>");
  36:             sb.Append("\r\n\t[-sitesource <URL to a source site master page file to copy to the target>");
  37:             Init(parameters, sb.ToString());
  38:         }
  40:         #region ISPStsadmCommand Members
  42:         /// <summary>
  43:         /// Gets the help message.
  44:         /// </summary>
  45:         /// <param name="command">The command.</param>
  46:         /// <returns></returns>
  47:         public override string GetHelpMessage(string command)
  48:         {
  49:             return HelpMessage;
  50:         }
  52:         /// <summary>
  53:         /// Runs the specified command.
  54:         /// </summary>
  55:         /// <param name="command">The command.</param>
  56:         /// <param name="keyValues">The key values.</param>
  57:         /// <param name="output">The output.</param>
  58:         /// <returns></returns>
  59:         public override int Execute(string command, StringDictionary keyValues, out string output)
  60:         {
  61:             output = string.Empty;
  62:             Verbose = true;
  64:             string url = Params["url"].Value.TrimEnd('/');
  65:             string siteMaster = null;
  66:             string systemMaster = null;
  67:             string siteSource = null;
  68:             string systemSource = null;
  69:             bool recurse = Params["resetsubsites"].UserTypedIn;
  71:             if (Params["sitemaster"].UserTypedIn)
  72:                 siteMaster = Params["sitemaster"].Value;
  74:             if (Params["systemmaster"].UserTypedIn)
  75:                 systemMaster = Params["systemmaster"].Value;
  78:             if (Params["sitesource"].UserTypedIn)
  79:                 siteSource = Params["sitesource"].Value;
  81:             if (Params["systemsource"].UserTypedIn)
  82:                 systemSource = Params["systemsource"].Value;
  85:             SetMasterPages(url, siteSource, systemSource, siteMaster, systemMaster, recurse);
  87:             return OUTPUT_SUCCESS;
  88:         }
  92:         /// <summary>
  93:         /// Validates the specified key values.
  94:         /// </summary>
  95:         /// <param name="keyValues">The key values.</param>
  96:         public override void Validate(StringDictionary keyValues)
  97:         {
  98:             base.Validate(keyValues);
 100:             if (!Params["sitemaster"].UserTypedIn && !Params["systemmaster"].UserTypedIn)
 101:             {
 102:                 throw new SPSyntaxException("You must provide at least one of the sitemaster or systemmaster parameters.");
 103:             }
 104:         }
 106:         #endregion
 108:         /// <summary>
 109:         /// Validates the master page URL.
 110:         /// </summary>
 111:         /// <param name="site">The site.</param>
 112:         /// <param name="masterPageUrl">The master page URL.</param>
 113:         /// <param name="source">The source.</param>
 114:         private static void ValidateMasterPageUrl(SPSite site, ref string masterPageUrl, string source)
 115:         {
 116:             if (!string.IsNullOrEmpty(masterPageUrl))
 117:             {
 118:                 masterPageUrl = masterPageUrl.ToLowerInvariant();
 119:                 if (masterPageUrl.IndexOf("_catalogs/masterpage") < 0)
 120:                     throw new ArgumentException(string.Format("The specified master page url is not in the '_catalogs/masterpage' gallery: {0}", masterPageUrl));
 121:                 if (!masterPageUrl.EndsWith(".master"))
 122:                     throw new ArgumentException(string.Format("The specified master page url does not end with '.master': {0}", masterPageUrl));
 124:                 if (!string.IsNullOrEmpty(source) && site.MakeFullUrl(masterPageUrl).ToLowerInvariant() == source.ToLowerInvariant())
 125:                 {
 126:                     Log("WARNING: Source file and target are the same.  Source will not be copied: {0}", source);
 127:                     source = null;
 128:                 }
 129:                 SPFile sourceFile = null;
 130:                 string sourceList = null;
 131:                 if (!string.IsNullOrEmpty(source))
 132:                 {
 133:                     source = source.ToLowerInvariant();
 134:                     if (source.IndexOf("_catalogs/masterpage") < 0)
 135:                         throw new ArgumentException(string.Format("The specified source master page url is not in the '_catalogs/masterpage' gallery: {0}", source));
 136:                     if (!source.EndsWith(".master"))
 137:                         throw new ArgumentException(string.Format("The specified source master page url does not end with '.master': {0}", source));
 139:                     string sourceFileName = source.Substring(source.LastIndexOf('/') + 1);
 140:                     string targetFileName = masterPageUrl.Substring(masterPageUrl.LastIndexOf('/') + 1);
 141:                     if (sourceFileName != targetFileName)
 142:                         throw new ArgumentException(string.Format("The specified source filename ({0}) does not match the master page settings filename ({1}).", sourceFileName, targetFileName));
 144:                     // Get the source file to copy to the target.
 145:                     string sourceWebUrl = source.Substring(0, source.IndexOf("_catalogs/masterpage")).TrimEnd('/');
 146:                     using (SPSite sourceSite = new SPSite(sourceWebUrl))
 147:                     using (SPWeb sourceWeb = sourceSite.OpenWeb())
 148:                     {
 149:                         sourceFile = sourceWeb.GetFile(Utilities.GetServerRelUrlFromFullUrl(source));
 150:                         if (!sourceFile.Exists)
 151:                             throw new FileNotFoundException(string.Format("The specified source file does not exist: {0}", source));
 152:                         sourceList = sourceSite.MakeFullUrl(sourceFile.Item.ParentList.RootFolder.ServerRelativeUrl);
 153:                     }
 154:                 }
 156:                 // Get the web associated with the passed in master page (we can't use OpenWeb(masterPageUrl, false) because it will throw an exception as it doesn't allow opening webs using this url).
 157:                 string masterWebUrl = masterPageUrl.Substring(0, masterPageUrl.IndexOf("_catalogs/masterpage")).TrimEnd('/');
 158:                 using (SPWeb masterWeb = site.OpenWeb(masterWebUrl))
 159:                 {
 160:                     try
 161:                     {
 162:                         if (sourceFile != null)
 163:                         {
 164:                             SPList masterPageGallery = masterWeb.GetList(masterWebUrl + "/_catalogs/masterpage");
 165:                             string targetList = site.MakeFullUrl(masterPageGallery.RootFolder.ServerRelativeUrl);
 166:                             CopyListItem copyCmd = new CopyListItem();
 167:                             List<int> ids = new List<int> {sourceFile.Item.ID};
 168:                             Log("Progress: Copying source file ({0}) to target ({1})...", source, targetList);
 169:                             copyCmd.CopyItem(ids, sourceList, targetList, false, false, true, SPIncludeVersions.CurrentVersion, SPIncludeDescendants.Content, SPUpdateVersions.Append);
 170:                         }
 171:                         // Try to locate the file specified.
 172:                         SPFile file = masterWeb.GetFile(masterPageUrl);
 173:                         if (!file.Exists)
 174:                             throw new FileNotFoundException();
 176:                         // The master page settings page is case sensitive so we need to make sure that the case of the string matches the actual file name.
 177:                         // This is crude but it works (we have to use the file.Item.Url property instead of file.ServerRelativeUrl because the latter returns
 178:                         // whatever we provided it.
 179:                         masterPageUrl = masterWeb.ServerRelativeUrl.TrimEnd('/') + "/" + file.Item.Url;
 180:                     }
 181:                     catch
 182:                     {
 183:                         throw new FileNotFoundException(string.Format("The specified master page could not be found: {0}", masterPageUrl));
 184:                     }
 186:                 }
 187:             }
 188:         }
 190:         /// <summary>
 191:         /// Sets the master pages.
 192:         /// </summary>
 193:         /// <param name="url">The URL.</param>
 194:         /// <param name="siteMaster">The site master.</param>
 195:         /// <param name="systemMaster">The system master.</param>
 196:         /// <param name="recurse">if set to <c>true</c> [recurse].</param>
 197:         public static void SetMasterPages(string url, string siteMaster, string systemMaster, bool recurse)
 198:         {
 199:             SetMasterPages(url, null, null, siteMaster, systemMaster, recurse);
 200:         }
 202:         /// <summary>
 203:         /// Sets the master pages.
 204:         /// </summary>
 205:         /// <param name="url">The URL.</param>
 206:         /// <param name="siteSource">The site source.</param>
 207:         /// <param name="systemSource">The system source.</param>
 208:         /// <param name="siteMaster">The site master.</param>
 209:         /// <param name="systemMaster">The system master.</param>
 210:         /// <param name="recurse">if set to <c>true</c> [recurse].</param>
 211:         public static void SetMasterPages(string url, string siteSource, string systemSource, string siteMaster, string systemMaster, bool recurse)
 212:         {
 213:             using (SPSite site = new SPSite(url))
 214:             using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
 215:             {
 216:                 Log("Progress: Processing Site Collection {0}...", url);
 218:                 // If the source files are the same then clear the system source so that we only do the copying once.
 219:                 if (siteSource != null && systemSource != null && siteSource.ToLowerInvariant() == systemSource.ToLowerInvariant())
 220:                     systemSource = null;
 223:                 // Because of the way the validation code works and because we don't want to have to specify the source twice (thus copying
 224:                 // the file twice) then we have to make sure that if a source is not set then it is done last.
 225:                 if ((siteSource == null && systemSource == null) || (siteSource != null))
 226:                 {
 227:                     ValidateMasterPageUrl(site, ref siteMaster, siteSource);
 228:                     ValidateMasterPageUrl(site, ref systemMaster, systemSource);
 229:                 }
 230:                 else
 231:                 {
 232:                     ValidateMasterPageUrl(site, ref systemMaster, systemSource);
 233:                     ValidateMasterPageUrl(site, ref siteMaster, siteSource);
 234:                 }
 236:                 SetMasterPages(web, siteMaster, systemMaster, recurse);
 237:             }
 238:         }
 240:         /// <summary>
 241:         /// Sets the master pages.
 242:         /// </summary>
 243:         /// <param name="web">The web.</param>
 244:         /// <param name="siteMaster">The site master.</param>
 245:         /// <param name="systemMaster">The system master.</param>
 246:         /// <param name="recurse">if set to <c>true</c> [recurse].</param>
 247:         private static void SetMasterPages(SPWeb web, string siteMaster, string systemMaster, bool recurse)
 248:         {
 249:             Log("Progress: Processing Web {0}...", web.Url);
 250:             if (!string.IsNullOrEmpty(siteMaster) && web.CustomMasterUrl != siteMaster)
 251:             {
 252:                 Log("Progress: Changing Site Master from '{0}' to '{1}'.", web.CustomMasterUrl, siteMaster);
 253:                 web.CustomMasterUrl = siteMaster;
 254:             }
 256:             if (!string.IsNullOrEmpty(systemMaster) && web.MasterUrl != systemMaster)
 257:             {
 258:                 Log("Progress: Changing System Master from '{0}' to '{1}'.", web.MasterUrl, systemMaster);
 259:                 web.MasterUrl = systemMaster;
 260:             }
 262:             if (recurse)
 263:             {
 264:                 foreach (SPWeb subWeb in web.Webs)
 265:                 {
 266:                     try
 267:                     {
 268:                         SetMasterPages(subWeb, siteMaster, systemMaster, recurse);
 269:                     }
 270:                     finally
 271:                     {
 272:                         subWeb.Dispose();
 273:                     }
 274:                 }
 275:             }
 276:             web.Update();
 277:         }
 278:     }
 279: }

The help for the command is shown below:

C:\>stsadm -help gl-setmasterpage

stsadm -o gl-setmasterpage

Sets the site and/or system master page for the given web.

        -url <web URL>
        [-sitemaster <server relative URL to the site master page>]
        [-systemmaster <server relative URL to the system master page>]
        [-systemsource <URL to a source system master page file to copy to the target>
        [-sitesource <URL to a source site master page file to copy to the target>

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-setmasterpage WSS 3.0, MOSS 2007 Released: 2/7/2009

Parameter Name Short Form Required Description Example Usage
url   Yes URL of the web or site collection to update. -url "http://portal"
sitemaster sitemp Yes if systemmaster is not provided or sitesource is provided The server relative URL to the master page. -sitemaster "/_catalogs/masterpage/default.master"

-sitemp "/_catalogs/masterpage/default.master"
systemmaster sysmp Yes if sitemaster is not provided or systemsource is provided The server relative URL to the master page. -systemmaster "/_catalogs/masterpage/default.master"

-sysmp "/_catalogs/masterpage/default.master"
resetsubsites reset No If specified all sub-sites of the passed in URL will be configured to use the specified master page. -resetsubsites

systemsource syssrc No The absolute URL to the source master page to copy to the master page gallery of the web specified in the URL parameter. -systemsource "http://portal/_catalogs/masterpage/default.master"

-syssrc "http://portal/_catalogs/masterpage/default.master"
sitesource sitesrc No The absolute URL to the source master page to copy to the master page gallery of the web specified in the URL parameter. -sitesource "http://portal/_catalogs/masterpage/default.master"

-sitesrc "http://portal/_catalogs/masterpage/default.master"

Now that we have the command we can easily combine this with a simple bit of PowerShell that will enable us to copy a master page from a single source site to all sites that match our filter criteria and set the master page settings to use this new master page.  The code to do this is shown in lines 1-6 below along with some sample output in the following lines:

   1: PS C:\> $sites = get-spsite-gl -url http://portal*
   2: PS C:\> foreach ($site in $sites) {
   3: >> $siteMaster = $site.ServerRelativeUrl.TrimEnd('/') + "/_catalogs/masterpage/gary.master"
   4: >> stsadm -o gl-setmasterpage -url ($site.Url) -sitemaster $siteMaster -sitesource http://portal/_catalogs/masterpage/gary.master -reset
   5: >> }
   6: >>
   8: Progress: Processing Site Collection http://portal...
   9: WARNING: Source file and target are the same.  Source will not be copied: http://portal/_catalogs/masterpage/gary.master
  11: Progress: Processing Web http://portal...
  12: Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
  13: Progress: Processing Web http://portal/Docs...
  14: Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
  15: Progress: Processing Web http://portal/News...
  16: Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
  17: Progress: Processing Web http://portal/Reports...
  18: Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
  19: Progress: Processing Web http://portal/SearchCenter...
  20: Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
  21: Progress: Processing Web http://portal/SiteDirectory...
  22: Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
  23: Operation completed successfully.
  26: Progress: Processing Site Collection http://portal/sites/Test...
  27: Progress: Copying source file (http://portal/_catalogs/masterpage/gary.master) to target (http://portal/sites/test/_catalogs/masterpage)...
  28: Progress: Processing Web http://portal/sites/Test...
  29: Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
  30: Progress: Processing Web http://portal/sites/Test/Docs...
  31: Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
  32: Progress: Processing Web http://portal/sites/Test/News...
  33: Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
  34: Progress: Processing Web http://portal/sites/Test/Reports...
  35: Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
  36: Progress: Processing Web http://portal/sites/Test/SearchCenter...
  37: Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
  38: Progress: Processing Web http://portal/sites/Test/SiteDirectory...
  39: Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
  40: Operation completed successfully.
  42: PS C:\>


Paul said...

Like the approach, but what about handling the workspace sites which employ slightly different functionality for that site templates features.

Gary Lapointe said...

I haven't looked at that specific template - can you elaborate on what your concern is? My understanding is that the master page settings are not related to the site template in any way (the settings are SPWeb specific) so I'm not sure what issue you'd have (that template does use a different master page than default.master but as far as know it's still assigned in the same way).

Sweet Sparky said...

Hi gary,
I used the gl-setmasterpage. works fine except that if does not get all the site collections. Seems to be randon. It did some sub sites, Some Site collections under the applications. I did check the permissions, amd e some them same and tried. but it seem to skipping some. These were migrated from moss. Have you seen this before.

Gary Lapointe said...

The command only works for one site collection at a time. If you want to do more than one then I'd suggest you combine it with powershell. I'm not sure why it's not resetting your sub-sites though - could be a permissions thing (make sure you run as a farm admin).

Sweet Sparky said...

My apologies Gary. Please ignore my post about '-'. It was actually complaining about : in sitesource. However, I the operation is completed. However when I go to site collection is the my.master is still not set as the default. Even when I try creating a new page. Am I missing anything? (the page got deployed though.

Alexandr said...

Thank you so much! You did my life easier =)

Khaled said...

Hmm. How about resetting the subsites to inherit the master page rather than actually setting the master page on the subsites?

Michael M. said...

Hey Gary, do you have a matching type of command that also allows you to set/push down the Alternate CSS URL along with the Master Page? I'm wandering through your command index but I'm not finding anything...thanks! - M

FelipeLodi.Com said...

Hi Gary,

I have no words to express how your Tools are Useful here in my Environment. Many, many thanks.

Regarding to this Command, I've tried to copy different Pages instead MasterPages, but with no sucess.

Do you have any command to do the same copy, for example, WelcomeSplash.aspx?

(say, to copy this page to different WebApp as I did with my MasterPages)


Gary Lapointe said...

You can use the gl-exportlistitem and gl-importlistitem commands.

Gary Lapointe said...

Michael- I don't have a command for the css but you could do this pretty easily using powershell.

Matteo said...

HY Gary.. GREAT Work.
Please i want know if i can install powershell and stsadm exstension on the same version? Thanks.

Gary Lapointe said...

You can use powershell and stsadm (stsadm is just an executable that takes in arguments so you can use it from a command prompt or a powershell window). As such, you can use either my stsadm extensions or my powershell cmdlets together.