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:

  1using System;
  2using System.Collections.Generic;
  3using System.Collections.Specialized;
  4using System.IO;
  5using System.Text;
  6using Lapointe.SharePoint.STSADM.Commands.Lists;
  7using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
  8using Lapointe.SharePoint.STSADM.Commands.SPValidators;
  9using Microsoft.SharePoint;
 10using Microsoft.SharePoint.Deployment;
 11 
 12namespace 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()));
 28 
 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        }
 39 
 40        #region ISPStsadmCommand Members
 41 
 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        }
 51 
 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;
 63 
 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;
 70 
 71            if (Params["sitemaster"].UserTypedIn)
 72                siteMaster = Params["sitemaster"].Value;
 73 
 74            if (Params["systemmaster"].UserTypedIn)
 75                systemMaster = Params["systemmaster"].Value;
 76 
 77 
 78            if (Params["sitesource"].UserTypedIn)
 79                siteSource = Params["sitesource"].Value;
 80 
 81            if (Params["systemsource"].UserTypedIn)
 82                systemSource = Params["systemsource"].Value;
 83 
 84 
 85            SetMasterPages(url, siteSource, systemSource, siteMaster, systemMaster, recurse);
 86 
 87            return OUTPUT_SUCCESS;
 88        }
 89 
 90       
 91 
 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);
 99 
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        }
105 
106        #endregion
107 
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));
123 
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));
138 
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));
143 
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                }
155 
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();
175 
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                    }
185 
186                }
187            }
188        }
189 
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        }
201 
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);
217 
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;
221 
222 
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                }
235 
236                SetMasterPages(web, siteMaster, systemMaster, recurse);
237            }
238        }
239 
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            }
255 
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            }
261 
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.

Parameters:
        -url <web URL>
        [-sitemaster <server relative URL to the site master page>]
        [-systemmaster <server relative URL to the system master page>]
        [-resetsubsites]
        [-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 NameAvailabilityBuild Date
gl-setmasterpageWSS 3.0, MOSS 2007Released: 2/7/2009
Parameter NameShort FormRequiredDescriptionExample Usage
urlYesURL of the web or site collection to update.-url "http://portal"
sitemastersitempYes if systemmaster is not provided or sitesource is providedThe server relative URL to the master page.-sitemaster "/_catalogs/masterpage/default.master", -sitemp "/_catalogs/masterpage/default.master"
systemmastersysmpYes if sitemaster is not provided or systemsource is providedThe server relative URL to the master page.-systemmaster "/_catalogs/masterpage/default.master", -sysmp "/_catalogs/masterpage/default.master"
resetsubsitesresetNoIf specified all sub-sites of the passed in URL will be configured to use the specified master page.-resetsubsites, -reset
systemsourcesyssrcNoThe 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"
sitesourcesitesrcNoThe 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:

PS C:\> $sites = get-spsite-gl -url http://portal*
PS C:\> foreach ($site in $sites) {
>> $siteMaster = $site.ServerRelativeUrl.TrimEnd('/') + "/_catalogs/masterpage/gary.master"
>> stsadm -o gl-setmasterpage -url ($site.Url) -sitemaster $siteMaster -sitesource http://portal/_catalogs/masterpage/gary.master -reset
>> }
>>
 
Progress: Processing Site Collection http://portal...
WARNING: Source file and target are the same.  Source will not be copied: http://portal/_catalogs/masterpage/gary.master
 
Progress: Processing Web http://portal...
Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
Progress: Processing Web http://portal/Docs...
Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
Progress: Processing Web http://portal/News...
Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
Progress: Processing Web http://portal/Reports...
Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
Progress: Processing Web http://portal/SearchCenter...
Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
Progress: Processing Web http://portal/SiteDirectory...
Progress: Changing Site Master from '/_catalogs/masterpage/default.master' to '/_catalogs/masterpage/gary.master'.
Operation completed successfully.
 
 
Progress: Processing Site Collection http://portal/sites/Test...
Progress: Copying source file (http://portal/_catalogs/masterpage/gary.master) to target (http://portal/sites/test/_catalogs/masterpage)...
Progress: Processing Web http://portal/sites/Test...
Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
Progress: Processing Web http://portal/sites/Test/Docs...
Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
Progress: Processing Web http://portal/sites/Test/News...
Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
Progress: Processing Web http://portal/sites/Test/Reports...
Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
Progress: Processing Web http://portal/sites/Test/SearchCenter...
Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
Progress: Processing Web http://portal/sites/Test/SiteDirectory...
Progress: Changing Site Master from '/sites/Test/_catalogs/masterpage/default.master' to '/sites/test/_catalogs/masterpage/gary.master'.
Operation completed successfully.
 
PS C:\>