MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. 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.

Wednesday, August 29, 2007

Fix Publishing Pages Page Layout URL

This command was created to fix an issue with our upgraded sites as well as issues I discovered with sites that had been imported to new farms. What happened was that after the upgrade (or an import) the Page Layout URL (page.ListItem[FieldId.PageLayout]) for various publishing pages was pointing to the wrong place. This didn't prevent the page from loading as the Layout property of the page was referencing the correct PageLayout object - what failed was the editing of the page settings (so edit a page and click Page->Page Settings).

When I attempted to edit the page settings I'd get an error due to this url being incorrect (pointed to the old location). So rather than create something one off I figured I'd make this a command that I could use later as I know I'll need it any time I decide to move a site to a new site collection or farm. The command I created, gl-fixpublishingpagespagelayouturl, is detailed below (apologies for the verbose name).

The code is pretty simple - I'm just resetting the URL based on what I know it should be (I'm using the existing page layout filename and just fixing the host and site collection path). You can also pass in a known page layout url or a regular expression search and replace string to use. I suggest you test this in a virtual environment or at least by using the test parameter to see what will change before running on your entire intranet. The core code is shown below (I left out the details about how I'm looping through the sites - download the code if you'd like to see that):

   1: public static void FixPages(PublishingWeb publishingWeb, string pageName, string pageLayoutUrl, Regex searchRegex, string replaceString, bool verbose, bool fixContact, bool test)
   2: {
   3:     if (!PublishingWeb.IsPublishingWeb(publishingWeb.Web))
   4:         return;
   5:  
   6:     PublishingPageCollection pages;
   7:     int tryCount = 0;
   8:     while (true)
   9:     {
  10:         try
  11:         {
  12:             tryCount++;
  13:             pages = publishingWeb.GetPublishingPages();
  14:             break;
  15:         }
  16:         catch (InvalidPublishingWebException)
  17:         {
  18:             // The following is meant to deal with a timing issue when using this method in conjuction with other commands.  When
  19:             // used independently this should be unnecessary.
  20:             if (tryCount > 4)
  21:                 throw;
  22:             Thread.Sleep(10000);
  23:             SPWeb web = publishingWeb.Web;
  24:             SPSite site = web.Site;
  25:             string url = site.MakeFullUrl(web.ServerRelativeUrl);
  26:             site.Close();
  27:             site.Dispose();
  28:             web.Close();
  29:             web.Dispose();
  30:             publishingWeb.Close();
  31:             site = new SPSite(url);
  32:             web = site.OpenWeb(Utilities.GetServerRelUrlFromFullUrl(url));
  33:             publishingWeb = PublishingWeb.GetPublishingWeb(web);
  34:         }
  35:     }
  36:  
  37:     foreach (PublishingPage page in pages)
  38:     {
  39:         if (!(string.IsNullOrEmpty(pageName) || page.Name.ToLower() == pageName.ToLower()))
  40:             continue;
  41:  
  42:         if (verbose)
  43:         {
  44:             Log(string.Format("Begin processing {0}.", page.Url));
  45:             Log(string.Format("Current layout set to {0}.", page.ListItem[FieldId.PageLayout]));
  46:         }
  47:  
  48:         // Can't edit items that are checked out.
  49:         if (Utilities.IsCheckedOut(page.ListItem))
  50:         {
  51:             if (verbose)
  52:                 Log("Page is already checked out - skipping.");
  53:             continue;
  54:         }
  55:  
  56:         SPFieldUrlValue url;
  57:         if (string.IsNullOrEmpty(pageLayoutUrl))
  58:         {
  59:             if (searchRegex == null)
  60:             {
  61:                 if (page.ListItem[FieldId.PageLayout] == null || string.IsNullOrEmpty(page.ListItem[FieldId.PageLayout].ToString().Trim()))
  62:                 {
  63:                     if (verbose)
  64:                         Log(string.Format("Current page layout is empty - skipping.  Use the 'pagelayout' parameter to set a page layout."));
  65:  
  66:                     continue;
  67:                 }
  68:  
  69:                 // We didn't get a layout url passed in or a regular expression so try and fix the existing url
  70:                 url = new SPFieldUrlValue(page.ListItem[FieldId.PageLayout].ToString());
  71:                 if (string.IsNullOrEmpty(url.Url) ||
  72:                     url.Url.IndexOf("/_catalogs/") < 0)
  73:                 {
  74:                     if (verbose)
  75:                         Log(string.Format("Current page layout does not point to a _catalogs folder or is empty - skipping.  Use the 'pagelayout' parameter to set a page layout  Layout Url: {0}",
  76:                                 url));
  77:                     continue;
  78:                 }
  79:  
  80:  
  81:                 string newUrl = publishingWeb.Web.Site.ServerRelativeUrl.TrimEnd('/') +
  82:                               url.Url.Substring(url.Url.IndexOf("/_catalogs/"));
  83:  
  84:                 string newDesc = publishingWeb.Web.Site.MakeFullUrl(newUrl);
  85:  
  86:                 if (url.Url.ToLowerInvariant() == newUrl.ToLowerInvariant())
  87:                 {
  88:                     if (verbose)
  89:                         Log("Current layout matches new evaluated layout - skipping.");
  90:                     continue;
  91:                 }
  92:                 url.Url = newUrl;
  93:                 url.Description = newDesc;
  94:             }
  95:             else
  96:             {
  97:                 if (page.ListItem[FieldId.PageLayout] == null || string.IsNullOrEmpty(page.ListItem[FieldId.PageLayout].ToString().Trim()))
  98:                     if (verbose)
  99:                         Log(string.Format("Current page layout is empty - skipping.  Use the pagelayout parameter to set a page layout."));
 100:  
 101:                 // A regular expression was passed in so use it to fix the page layout url if we find a match.
 102:                 if (searchRegex.IsMatch((string)page.ListItem[FieldId.PageLayout]))
 103:                 {
 104:                     url = new SPFieldUrlValue(page.ListItem[FieldId.PageLayout].ToString());
 105:                     string newUrl = searchRegex.Replace((string)page.ListItem[FieldId.PageLayout], replaceString);
 106:                     if (url.ToString().ToLowerInvariant() == newUrl.ToLowerInvariant())
 107:                     {
 108:                         if (verbose)
 109:                             Log("Current layout matches new evaluated layout - skipping.");
 110:                         continue;
 111:                     }
 112:                     url = new SPFieldUrlValue(newUrl);
 113:                 }
 114:                 else
 115:                 {
 116:                     if (verbose)
 117:                         Log("Existing page layout url does not match provided regular expression - skipping.");
 118:                     continue;
 119:                 }
 120:             }
 121:         }
 122:         else
 123:         {
 124:             // The user passed in an url string so use it.
 125:             if (pageLayoutUrl.ToLowerInvariant() == (string)page.ListItem[FieldId.PageLayout])
 126:             {
 127:                 if (verbose)
 128:                     Log("Current layout matches provided layout - skipping.");
 129:                 continue;
 130:             }
 131:  
 132:             url = new SPFieldUrlValue(pageLayoutUrl);
 133:         }
 134:  
 135:         string fileName = url.Url.Substring(url.Url.LastIndexOf('/'));
 136:         // Make sure that the URLs are server relative instead of absolute.
 137:         if (url.Description.ToLowerInvariant().StartsWith("http"))
 138:             url.Description = Utilities.GetServerRelUrlFromFullUrl(url.Description) + fileName;
 139:         if (url.Url.ToLowerInvariant().StartsWith("http"))
 140:             url.Url = Utilities.GetServerRelUrlFromFullUrl(url.Url) + fileName;
 141:  
 142:         if (page.ListItem[FieldId.PageLayout] != null && url.ToString().ToLowerInvariant() == page.ListItem[FieldId.PageLayout].ToString().ToLowerInvariant())
 143:             continue; // No difference detected so move on.
 144:  
 145:         if (verbose)
 146:             Log(string.Format("Changing layout url from \"{0}\" to \"{1}\"", page.ListItem[FieldId.PageLayout], url));
 147:  
 148:  
 149:         if (fixContact)
 150:         {
 151:             SPUser contact = null;
 152:             try
 153:             {
 154:                 contact = page.Contact;
 155:             }
 156:             catch (SPException)
 157:             {
 158:             }
 159:             if (contact == null)
 160:             {
 161:                 if (verbose)
 162:                     Log(string.Format("Page contact ('{0}') does not exist - assigning current user as contact.", page.ListItem[FieldId.Contact]));
 163:                 page.Contact = publishingWeb.Web.CurrentUser;
 164:  
 165:                 if (!test)
 166:                     page.ListItem.SystemUpdate();
 167:             }
 168:         }
 169:  
 170:         if (test)
 171:             continue;
 172:  
 173:         page.CheckOut();
 174:         page.ListItem[FieldId.PageLayout] = url;
 175:         page.ListItem.UpdateOverwriteVersion();
 176:         PublishItems.Settings settings = new PublishItems.Settings();
 177:         settings.Test = test;
 178:         settings.Quiet = !verbose;
 179:         settings.LogFile = null;
 180:  
 181:         PublishItems.PublishListItem(page.ListItem, page.ListItem.ParentList, settings, "stsadm -o fixpublishingpagespagelayouturl");
 182:         //page.ListItem.File.CheckIn("Fixed URL to page layout.", SPCheckinType.MajorCheckIn);
 183:         //if (page.ListItem.ModerationInformation != null)
 184:         //    page.ListItem.File.Approve("Publishing changes to page layout.");
 185:     }
 186: }

The syntax of the command can be seen below:

C:\>stsadm -help gl-fixpublishingpagespagelayouturl

stsadm -o gl-fixpublishingpagespagelayouturl


Fixes the Page Layout URL property of publishing pages which can get messed up during an upgrade or from importing into a new farm.

Parameters:
        -url <url>
        -scope <WebApplication | Site | Web | Page>
        [-pagename <if scope is Page, the name of the page to update>]
        {[-pagelayout <url of page layout to retarget page(s) to (format: "url, desc")>] /
         [-regexsearchstring <search pattern to use for a regular expression replacement of the page layout url>]
         [-regexreplacestring <replace pattern to use for a regular expression replacement of the page layout url>]}
        [-verbose]
        [-test]

To fix all the pages on a given web application you would use the following syntax:

stsadm –o gl-fixpublishingpagespagelayouturl –url "http://intranet/" -scope webapplication

Update 2/17/2008: I've made quite a few changes to this command. Note that if you have a previous version the syntax of the command has changed a lot (content above has been updated). You can now pass in a single url parameter along with a scope parameter. Also - you can now pass in a test parameter to simulate what changes would occur. The verbose switch will tell you what it's doing. If you know you want to set a specific page to you can pass in the pagename parameter. Similarly if you want to set a specific page layout url use the pagelayout parameter or you can use the regex parameters for doing a search and replace. Note that if you pass in the pagelayout parameter you want to use the format "[url], [desc]" - for example:

stsadm -o gl-fixpublishingpagespagelayouturl -url "http://intranet" -scope site -pagelayout "http://intranet/_catalogs/masterpage/WelcomeLinks.aspx, /_catalogs/masterpage/WelcomeLinks.aspx".

Enumerate Available Page Layouts

I created this only because I needed to debug some issues I've been having with Page Layouts - try to convert a sub-site to a site collection and you'll see what I mean :). I doubt this command will be very useful to anyone but seeing as I've got it coded and working there was no sense in pulling the code. The command itself, gl-enumavailablepagelayouts, is very simple - it just outputs the available page layouts by calling GetAvailablePageLayouts() from a PublishingWeb object.

I'm outputting this code in XML as I wanted to display more things than what made sense in a flat file list (I suppose someone could use the output of this command for something else). The core code is shown below:

   1: string url = keyValues["url"];
   2: XmlDocument xmlDoc = new XmlDocument();
   3: xmlDoc.AppendChild(xmlDoc.CreateElement("PageLayouts"));
   4: // I added formatting just to make the xml easier to read when looking at it via the console.
   5: xmlDoc.DocumentElement.AppendChild(xmlDoc.CreateWhitespace("\r\n"));
   6:  
   7: using (SPSite site = new SPSite(url))
   8: using (SPWeb web = site.OpenWeb())
   9: {
  10:     PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
  11:  
  12:     foreach (PageLayout layout in pubweb.GetAvailablePageLayouts())
  13:     {
  14:         xmlDoc.DocumentElement.AppendChild(xmlDoc.CreateWhitespace("\t"));
  15:         XmlElement layoutNode = xmlDoc.CreateElement("PageLayout");
  16:         layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
  17:  
  18:         XmlElement node = xmlDoc.CreateElement("Name");
  19:         node.InnerText = layout.Name;
  20:         layoutNode.AppendChild(node);
  21:         layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
  22:  
  23:         node = xmlDoc.CreateElement("Title");
  24:         node.InnerText = layout.Title;
  25:         layoutNode.AppendChild(node);
  26:         layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
  27:  
  28:         node = xmlDoc.CreateElement("Id");
  29:         node.InnerText = layout.ListItem.ID.ToString();
  30:         layoutNode.AppendChild(node);
  31:         layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
  32:  
  33:         node = xmlDoc.CreateElement("AssociatedContentType");
  34:         if (layout.AssociatedContentType != null)
  35:             node.InnerText = layout.AssociatedContentType.Name;
  36:         layoutNode.AppendChild(node);
  37:         layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
  38:  
  39:         node = xmlDoc.CreateElement("ContentType");
  40:         if (layout.ListItem[FieldId.ContentType] != null)
  41:             node.InnerText = layout.ListItem[FieldId.ContentType].ToString();
  42:         layoutNode.AppendChild(node);
  43:         layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
  44:  
  45:         node = xmlDoc.CreateElement("Hidden");
  46:         if (layout.ListItem[FieldId.Hidden] != null)
  47:             node.InnerText = layout.ListItem[FieldId.Hidden].ToString();
  48:         else
  49:             node.InnerText = "false";
  50:         layoutNode.AppendChild(node);
  51:         layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
  52:  
  53:         node = xmlDoc.CreateElement("FileUrl");
  54:         if (layout.ListItem.File != null)
  55:             node.InnerText = layout.ListItem.File.Url;
  56:         layoutNode.AppendChild(node);
  57:         layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t"));
  58:  
  59:         xmlDoc.DocumentElement.AppendChild(layoutNode);
  60:         xmlDoc.DocumentElement.AppendChild(xmlDoc.CreateWhitespace("\r\n"));
  61:     }
  62: }
  63: output += xmlDoc.OuterXml;

The syntax of the command can be seen below:

C:\>stsadm -help gl-enumavailablepagelayouts

stsadm -o gl-enumavailablepagelayouts

Returns the list of page layouts available for the given site collection.

Parameters:
        -url <site collection url>

Here’s an example of how to return the avilable page layouts for a publishing site site collection:

stsadm –o gl-enumavailablepagelayouts –url "http://intranet/"

The results of running the above command can be seen below:

   1: <PageLayouts>
   2:         <PageLayout>
   3:                 <Name>PageFromDocLayout.aspx</Name>
   4:                 <Title>Article page with body only</Title>
   5:                 <Id>24</Id>
   6:                 <AssociatedContentType>Article Page</AssociatedContentType>
   7:                 <ContentType>Page Layout</ContentType>
   8:                 <Hidden>false</Hidden>
   9:                 <FileUrl>_catalogs/masterpage/PageFromDocLayout.aspx</FileUrl>
  10:         </PageLayout>
  11:         <PageLayout>
  12:                 <Name>ArticleLeft.aspx</Name>
  13:                 <Title>Article page with image on left</Title>
  14:                 <Id>21</Id>
  15:                 <AssociatedContentType>Article Page</AssociatedContentType>
  16:                 <ContentType>Page Layout</ContentType>
  17:                 <Hidden>false</Hidden>
  18:                 <FileUrl>_catalogs/masterpage/ArticleLeft.aspx</FileUrl>
  19:         </PageLayout>
  20:         <PageLayout>
  21:                 <Name>ArticleRight.aspx</Name>
  22:                 <Title>Article page with image on right</Title>
  23:                 <Id>23</Id>
  24:                 <AssociatedContentType>Article Page</AssociatedContentType>
  25:                 <ContentType>Page Layout</ContentType>
  26:                 <Hidden>false</Hidden>
  27:                 <FileUrl>_catalogs/masterpage/ArticleRight.aspx</FileUrl>
  28:         </PageLayout>
  29:         <PageLayout>
  30:                 <Name>ArticleLinks.aspx</Name>
  31:                 <Title>Article page with summary links</Title>
  32:                 <Id>22</Id>
  33:                 <AssociatedContentType>Article Page</AssociatedContentType>
  34:                 <ContentType>Page Layout</ContentType>
  35:                 <Hidden>false</Hidden>
  36:                 <FileUrl>_catalogs/masterpage/ArticleLinks.aspx</FileUrl>
  37:         </PageLayout>
  38:         <PageLayout>
  39:                 <Name>RedirectPageLayout.aspx</Name>
  40:                 <Title>Redirect Page</Title>
  41:                 <Id>27</Id>
  42:                 <AssociatedContentType>Redirect Page</AssociatedContentType>
  43:                 <ContentType>Page Layout</ContentType>
  44:                 <Hidden>false</Hidden>
  45:                 <FileUrl>_catalogs/masterpage/RedirectPageLayout.aspx</FileUrl>
  46:         </PageLayout>
  47:         <PageLayout>
  48:                 <Name>AdvancedSearchLayout.aspx</Name>
  49:                 <Title>Advanced Search</Title>
  50:                 <Id>87</Id>
  51:                 <AssociatedContentType>Welcome Page</AssociatedContentType>
  52:                 <ContentType>Page Layout</ContentType>
  53:                 <Hidden>false</Hidden>
  54:                 <FileUrl>_catalogs/masterpage/AdvancedSearchLayout.aspx</FileUrl>
  55:         </PageLayout>
  56:         <PageLayout>
  57:                 <Name>BlankWebPartPage.aspx</Name>
  58:                 <Title>Blank Web Part Page</Title>
  59:                 <Id>28</Id>
  60:                 <AssociatedContentType>Welcome Page</AssociatedContentType>
  61:                 <ContentType>Page Layout</ContentType>
  62:                 <Hidden>false</Hidden>
  63:                 <FileUrl>_catalogs/masterpage/BlankWebPartPage.aspx</FileUrl>
  64:         </PageLayout>
  65:         <PageLayout>
  66:                 <Name>PeopleSearchResults.aspx</Name>
  67:                 <Title>People Search Results Page</Title>
  68:                 <Id>90</Id>
  69:                 <AssociatedContentType>Welcome Page</AssociatedContentType>
  70:                 <ContentType>Page Layout</ContentType>
  71:                 <Hidden>false</Hidden>
  72:                 <FileUrl>_catalogs/masterpage/PeopleSearchResults.aspx</FileUrl>
  73:  
  74:         </PageLayout>
  75:         <PageLayout>
  76:                 <Name>SearchMain.aspx</Name>
  77:                 <Title>Search Page</Title>
  78:                 <Id>88</Id>
  79:                 <AssociatedContentType>Welcome Page</AssociatedContentType>
  80:                 <ContentType>Page Layout</ContentType>
  81:                 <Hidden>false</Hidden>
  82:                 <FileUrl>_catalogs/masterpage/SearchMain.aspx</FileUrl>
  83:         </PageLayout>
  84:         <PageLayout>
  85:                 <Name>SearchResults.aspx</Name>
  86:                 <Title>Search Results Page</Title>
  87:                 <Id>89</Id>
  88:                 <AssociatedContentType>Welcome Page</AssociatedContentType>
  89:                 <ContentType>Page Layout</ContentType>
  90:                 <Hidden>false</Hidden>
  91:                 <FileUrl>_catalogs/masterpage/SearchResults.aspx</FileUrl>
  92:         </PageLayout>
  93:         <PageLayout>
  94:                 <Name>TabViewPageLayout.aspx</Name>
  95:                 <Title>Site Directory Home</Title>
  96:                 <Id>85</Id>
  97:                 <AssociatedContentType>Welcome Page</AssociatedContentType>
  98:                 <ContentType>Page Layout</ContentType>
  99:                 <Hidden>false</Hidden>
 100:                 <FileUrl>_catalogs/masterpage/TabViewPageLayout.aspx</FileUrl>
 101:         </PageLayout>
 102:         <PageLayout>
 103:                 <Name>WelcomeLinks.aspx</Name>
 104:                 <Title>Welcome page with summary links</Title>
 105:                 <Id>3</Id>
 106:                 <AssociatedContentType>Welcome Page</AssociatedContentType>
 107:                 <ContentType>Page Layout</ContentType>
 108:                 <Hidden>false</Hidden>
 109:                 <FileUrl>_catalogs/masterpage/WelcomeLinks.aspx</FileUrl>
 110:         </PageLayout>
 111:         <PageLayout>
 112:                 <Name>WelcomeTOC.aspx</Name>
 113:                 <Title>Welcome page with table of contents</Title>
 114:                 <Id>26</Id>
 115:                 <AssociatedContentType>Welcome Page</AssociatedContentType>
 116:                 <ContentType>Page Layout</ContentType>
 117:                 <Hidden>false</Hidden>
 118:                 <FileUrl>_catalogs/masterpage/WelcomeTOC.aspx</FileUrl>
 119:         </PageLayout>
 120:         <PageLayout>
 121:                 <Name>DefaultLayout.aspx</Name>
 122:                 <Title>Welcome page with Web Part zones</Title>
 123:                 <Id>83</Id>
 124:                 <AssociatedContentType>Welcome Page</AssociatedContentType>
 125:                 <ContentType>Page Layout</ContentType>
 126:                 <Hidden>false</Hidden>
 127:                 <FileUrl>_catalogs/masterpage/DefaultLayout.aspx</FileUrl>
 128:         </PageLayout>
 129:         <PageLayout>
 130:                 <Name>WelcomeSplash.aspx</Name>
 131:                 <Title>Welcome splash page</Title>
 132:                 <Id>25</Id>
 133:                 <AssociatedContentType>Welcome Page</AssociatedContentType>
 134:                 <ContentType>Page Layout</ContentType>
 135:                 <Hidden>false</Hidden>
 136:                 <FileUrl>_catalogs/masterpage/WelcomeSplash.aspx</FileUrl>
 137:         </PageLayout>
 138: </PageLayouts>