SharePoint Automation Gary Lapointe – Founding Partner, Aptillon, Inc.

4Sep/0815

Create a Publishing Page via STSADM

I'm working on a project which is going to require the creation of about a hundred publishing pages which were going to have to be created by hand.  I really didn't want to have to sit there going through the UI to create all those pages so I threw together a new STSADM command that I could use via a script to create the pages: gl-createpublishingpage.

Creating publishing pages via code is pretty simple - you just use the PublishingWeb object's GetPublishingPages() method to return back the collection of pages and then call its Add method passing in the page name and layout.  From there it's just a matter of setting the field properties.

   1: public static void CreatePage(string url, string pageName, string title, string layoutName, Dictionary<string, string> fieldDataCollection)
   2: {
   3:     using (SPSite site = new SPSite(url))
   4:     using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
   5:     {
   6:         if (!PublishingWeb.IsPublishingWeb(web))
   7:             throw new ArgumentException("The specified web is not a publishing web.");
   8:  
   9:         PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
  10:         PageLayout layout = null;
  11:         string availableLayouts = string.Empty;
  12:         foreach (PageLayout lo in pubweb.GetAvailablePageLayouts())
  13:         {
  14:             availableLayouts += "\t" + lo.Name + "\r\n";
  15:             if (lo.Name.ToLowerInvariant() == layoutName.ToLowerInvariant())
  16:             {
  17:                 layout = lo;
  18:                 break;
  19:             }
  20:         }
  21:         if (layout == null)
  22:             throw new ArgumentException("The layout specified could not be found.  Available layouts are:\r\n" + availableLayouts);
  23:  
  24:         if (!pageName.ToLowerInvariant().EndsWith(".aspx"))
  25:             pageName += ".aspx";
  26:         
  27:         PublishingPage page = pubweb.GetPublishingPages().Add(pageName, layout);
  28:         page.Title = title;
  29:         SPListItem item = page.ListItem;
  30:  
  31:         foreach (string fieldName in fieldDataCollection.Keys)
  32:         {
  33:             string fieldData = fieldDataCollection[fieldName];
  34:  
  35:             try
  36:             {
  37:                 SPField field = item.Fields.GetFieldByInternalName(fieldName);
  38:  
  39:                 if (field.ReadOnlyField)
  40:                 {
  41:                     Console.WriteLine("Field '{0}' is read only and will not be updated.", field.InternalName);
  42:                     continue;
  43:                 }
  44:  
  45:                 if (field.Type == SPFieldType.Computed)
  46:                 {
  47:                     Console.WriteLine("Field '{0}' is a computed column and will not be updated.", field.InternalName);
  48:                     continue;
  49:                 }
  50:  
  51:                 if (field.Type == SPFieldType.URL)
  52:                     item[field.Id] = new SPFieldUrlValue(fieldData);
  53:                 else if (field.Type == SPFieldType.User)
  54:                     AddListItem.SetUserField(web, item, field, fieldData);
  55:                 else
  56:                     item[field.Id] = fieldData;
  57:             }
  58:             catch (ArgumentException)
  59:             {
  60:                 Console.WriteLine("WARNING: Could not set field {0} for item {1}.", fieldName, item.ID);
  61:             }
  62:         }
  63:  
  64:         page.Update();
  65:     }
  66: }

The help for the command is shown below:

C:\>stsadm -help gl-createpublishingpage

stsadm -o gl-createpublishingpage


Creates a new publishing page.

Parameters:
        -url <url to the publishing web within which to create the page>
        -name <the filename of the page to create (do not include the extension)>
        -title <the page title>
        -layout <the filename of the page layout to use>
        [-fielddata <semi-colon separated list of key value pairs: "Field1=Val1;Field2=Val2"> (use ';;' to escape semi-colons in data values)]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-createpublishingpage MOSS 2007 Released: 9/4/2008

Parameter Name Short Form Required Description Example Usage
url   Yes The URL to the publishing web within which to create the page. -url http://portal
name n Yes The filename of the page to create.  It is not necessary to include the extension.  Do not use special characters. -name NewPage

-n NewPage
title t Yes The title of the page.  Wrap within quotes if includes spaces. -title "New Page"

-t "New Page"
layout l Yes The filename of the page layout to use. -layout DefaultLayout.aspx

-l DefaultLayout.aspx
fielddata fd No Key/Value pairs of metadata to apply to the new page.  Use the internal field name and separate multiple pairs with a semi-colon.  Use the following format when setting data: "Field1=Val1;Field2=Val2". -fielddata "PublishingContactEmail=user@domain.com;PublishingContactName=First Last"

-fd "PublishingContactEmail=user@domain.com;PublishingContactName=First Last"

The following is an example of how to add a new publishing page:

stsadm -o gl-createpublishingpage -url http://portal -name NewPage -title "New Page" -layout DefaultLayout.aspx

8Apr/0846

Fun with Variations

I've recently done a lot of work with my current client to get SharePoint variations working and as a result I've learned that you need to be very patient and persistent and keep a full bottle of Excedrin nearby.  Though this post is not directly related to stsadm extensions the results of my efforts on this project produced a couple of new commands which I'll touch upon here and detail in a follow-up post.  Before you begin working on a multilingual site using variations I highly suggest that you read the Building Multilingual Solutions Using 2007 SharePoint Products and Technologies white paper.

imageVariations in, a nutshell, allow you to present the same pages using different views of the page.  Though variations are typically used for multilingual sites they can also be used for other things such as media specific views.  For this post I'm going to create multilingual variation with the source variation being English and a single target variation using Korean.  The first thing we need to do is make sure that the operating system is ready for our language packs to be installed.  If you're using an East Asian language, such as Korean, then you will need to prep the OS, otherwise the OS should already be capable of handling the language pack.  To install the East Asian languages on the OS go to Start -> Control Panel -> Regional and Language Options.  On the "Languages" tab select the appropriate options and click OK.  This will install the necessary language files which will require a reboot when complete.

Now that the OS is prepared you are ready to install the language packs.  The install is real straightforward so I won't detail it here.  If you don't already have a web application created to host your variation sites you'll want to do that now.  Once you've identified your target web application you are ready to create your site collection.  It's important to note that the variations publishing feature is scoped at the site collection level - this means that you cannot have a source variation in one site collection and a target variation in another.  Also, the variation features are installed with the publishing feature so you must pick a publishing template when creating your initial site collection or activate the publishing features.  For your root site collection make sure you pick the language template that your administrators are going to be able to work with as your end-users will never see the root site collection stuff.

imageTo create your variation hierarchy you must first enable variations.  This is done by going to the root of the site collection and selecting "Site Actions -> Site Settings -> Modify All Settings -> Variations".  You first need to establish the variation root site - typically you'll just put a "/" which will make the root of site collection the variation root but you can make the variation root be a sub-site of the site collection.  The other options are fairly straightforward but the "Update Target Web Parts" and "Resources" options may need some clarification.  If you choose to update web parts with changes from the source be aware that any translations or changes that you may have made in the target variations will be overwritten with that of the source.  So if you're using a content editor web part and translating that content on the target sites then those translations will be overwritten when the page is propagated.  This may or may not be what you intend to happen.

One other thing that may require some additional clarification is the Resources option.  If you leave the option set to reference existing resources then there's essentially no change to the content - it will simply references images, for example, on the source site.  The copy option, however, will copy images to the target variations.  Note that if the image in question does not exist in the variations PublishingImages library then it won't be copied.  I've not had to deal with other resource types so I can't say what the limitations are for anything other than images.

imageOnce you've established that the site collection is to use variations you now need to create your variation labels.  When you initially create your labels you are not actually creating any instances of a site - you are just telling SharePoint how you want your sites to be created.  The sites are actually created later when you choose to create the variation hierarchy.  The first label you want to create is your source label.  It's important to note that once you've established your source you cannot change it later.  If you would a custom site template to be available here make sure that the FilterCategories property of your site template is set to PublishingSiteTemplate (you can find information about how to create your own publishing site templates here).  The "Label Name" field is what will appear in your URL - so if you specify "en-us" as the label name your url for the variation will look like "http://demo/en-us/".  For the "Hierarchy Creation" option you need to specify what you want copied when the label hierarchy is created.  The other options on this screen are very straightforward - you just need to specify the language template to use and the local and a name.

For this demo I've setup a source variation with a label name of "EN", "English" as the language template, "English (United States)" as the locale, "US-English" as the name, and "Publishing Site with Workflow" as the site template.  I've also chosen to copy the publishing site and all pages during hierarchy creation.

imageOnce your source variation is setup you can set up all of your target variations.  Note that you don't have to set the target variations up right away - you can create your hierarchy now and have it just create your source variation, get it looking/working the way you want and then add target variations as you are ready to handle them.  I would recommend though that you at least create one target variation so that you can work out the various issues that you're likely to encounter (see below) before you roll out your source variation.

For the purposes of this demonstration I'm going to create one target variation label which I'll name "KO".  The language template and locale will be set to "Korean".

imageOnce you've established your labels you are ready to create your variation hierarchy.  You do this by clicking the "Create Hierarchies" link on the "Variation Labels" page.

Now that you've got your variations created you can go to the site.  Notice that if you go to the root of the site collection (in my case "http://demo/" you will be automatically redirected to the variation whose locale matches that of your browser.  If SharePoint is unable to find a locale match then it will redirect you to the source variation.  The redirect is happening because SharePoint has replaced the welcome page of the root site collection with a new page called "VariationRoot.aspx".  It's important to note that the default.aspx page is still present and if a user navigates to that page they will see it so I recommend either deleting this page or adding a redirect web part or something similar to redirect users to the VariationRoot.aspx file so that it can do its thing.

Okay, so you've got your variations set up, now what?  Now it's time to create some content (realistically though you're going to want to work on branding and custom page layouts first but as there are tons of posts on how to do this I'm going to assume you've got that covered and are ready for creating content).  Editing the page in your source variation is no different than editing any publishing page.  However, once you publish the page the changes that you have made will be automatically copied to the variations.  Note that the act of copying the page is the result of an event receiver that is hooked up to the Pages library.  The event receiver will inspect the page and determine what needs to occur and then trigger a timer job to handle the actual copying of the page from the source library to all target libraries.  This timer job uses the Deployment API to copy the pages and if you've read any of my earlier posts you know that this imagedeployment API has numerous limitations (I'll touch upon a couple of problems below).  Because a timer job is used you won't see the pages propagated immediately - it could take several seconds to several minutes (I've even see it take several hours to show up).  If you'd like the page to be copied immediately then you can trigger it by selecting the "Update Variations" option from the "Tools" menu on the page editing toolbar.  If the page doesn't already exist in a variation you can use the "Submit a Variation..." link to submit the page to all variations.  If the page already exists in all variations then the "Submit a Variation..." link will effectively just work like the Create Page action.  Note that you can also use content deployment jobs to handle the translation of content by external agencies.  The XML that results from a content deployment job is a nightmare to work with but it wouldn't take much to provide a custom interface to that could be provided to your translators to work with the content.

image Now that you've got your changes made and you've created the variation, how do you get to that other pages?  One option is to simply edit the URL but you're users aren't going to know how to do that.  Fortunately there's a built in user control that we can enable called the Variation Label Control.  The control works via a SharePoint delegate control which is added to the master page of all publishing sites.  To enable the control you simply have to un-comment the server control tag in the "12\template\controltemplates\VariationsLabelMenu.ascx" file.  Once this has been enabled by un-commenting the server control tag then a drop down menu will appear at the top of the site allowing the user to jump to matching pages in other variations. 

It's important to understand how SharePoint relates the various pages to each other because in certain situations this relationship can get corrupted.  At the root of the site collection you can find a hidden list called "Relationships List".  This list contains entries which map the relationship between the various pages.  The behavior of the Variation Label Menu control is driven by this list so if the menu is not listing all pages then most likely it is because the Relationships List became corrupted (for example, an administrator on the Korean side decided to prevent changes from being propagated by deleting the page in the Korean Pages library and recreating it with new translated content).

imageLet's take a look at this hidden list - in our demo site we'll navigate to "http://demo/Relationships%20List/AllItems.aspx".  Once here we immediately see a few items with the title set to nothing (the first thing I usually do is create a new view showing at least the following columns: GroupID, ObjectID, and Deleted.  There will always be at least one entry in this list corresponding to the variation home which we set up earlier and defined as "/".  This entry, which will be the first item in the list, will always have "F68A02C8-2DCC-4894-B67D-BBAED5A066F9" as it's GroupID and whatever was set as the home path as it's ObjectID.  ParentAreaID has been empty in every example I've ever seen and does not appear to be used.  The GroupID field is used to group related elements.  So each variation label in the hierarchy will have the same GroupID and each page will have the same GroupID as its copied page in each variation.  The ObjectID is always the server relative url of the element (so the path to the variation label or page).

It's important to understand though that this is not the only place in which we find this information.  SharePoint will also store the GroupID and a backward link to an item in this list within two hidden fields on the page: PublishingVariationGroupID and Publishing VariationRelationshipLinkFieldID, respectively.  This information is also replicated in the MetaInfo property bag.  If we run my custom gl-exportlistitem2 command we can see what these values are for the default.aspx page:

C:\>stsadm -o gl-exportlistitem2 -url http://demo/en/pages/forms/allitems.aspx -path c:\exportdata\pages_en

The following is a snippet of the Manifest.xml file which results from running the above command (note that I've pulled some elements that were not relevant):

<Items SiteUrl="http://demo" WebUrl="/en">
  <Item IsPublishingPage="True" ContentTypeId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D400040A230B8F679045ADE877C7A560A0E7" PageLayout="WelcomeLinks.aspx" File="c:\exportdata\pages_en\Data\default.aspx" CreatedDate="4/3/2008 4:27:48 PM" LeafName="default.aspx" FolderUrl="">
    <Fields>
      <Field Name="Title">Home</Field>
      <Field Name="PublishingVariationGroupID">41ad5bd3-a882-4d50-a2ac-db5de6eb872c</Field>
      <Field Name="PublishingVariationRelationshipLinkFieldID">http://demo/Relationships List/4_.000, /Relationships List/4_.000</Field>
      <Field Name="MetaInfo">PublishingPageContent:SW|Test
vti_parserversion:SR|12.0.0.6219
vti_charset:SR|utf-8
vti_author:SR|
vti_setuppath:SX|SiteTemplates\\BLANKINTERNET\\default.aspx
vti_cachedneedsrewrite:BR|false
PublishingPageImage:SW|
SummaryLinks:SW|&lt;div title="_schemaversion" id="_3"&gt;\r\n  &lt;div title="_view"&gt;\r\n    &lt;span title="_columns"&gt;1&lt;/span&gt;\r\n    &lt;span title="_linkstyle"&gt;&lt;/span&gt;\r\n    &lt;span title="_groupstyle"&gt;&lt;/span&gt;\r\n  &lt;/div&gt;\r\n&lt;/div&gt;
vti_modifiedby:SR|SPDEV\\spadmin
vti_cachedhastheme:BR|false
PublishingVariationGroupID:SW|41ad5bd3-a882-4d50-a2ac-db5de6eb872c
SummaryLinks2:SW|&lt;div title="_schemaversion" id="_3"&gt;\r\n  &lt;div title="_view"&gt;\r\n    &lt;span title="_columns"&gt;1&lt;/span&gt;\r\n    &lt;span title="_linkstyle"&gt;&lt;/span&gt;\r\n    &lt;span title="_groupstyle"&gt;&lt;/span&gt;\r\n  &lt;/div&gt;\r\n&lt;/div&gt;
vti_cachedcustomprops:VX|PublishingPageImage PublishingPageContent SummaryLinks PublishingVariationGroupID SummaryLinks2 ContentType PublishingVariationRelationshipLinkFieldID vti_title PublishingPageLayout
ContentTypeId:SW|0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D400040A230B8F679045ADE877C7A560A0E7
PublishingVariationRelationshipLinkFieldID:SW|http://demo/Relationships List/4_.000, /Relationships List/4_.000
vti_cachedtitle:SR|Home
vti_title:SR|Home
ContentType:SW|Welcome Page
PublishingPageLayout:SW|/_catalogs/masterpage/WelcomeLinks.aspx, /_catalogs/masterpage/WelcomeLinks.aspx
</Field>
    </Fields>
  </Item>
</Items>

Knowing that this information is stored in two places like this becomes extremely important if you are trying to troubleshoot issues with the variation propagation process.  If the data in the Relationships List does not match up with the data in the page or if the related pages do not share the same GroupID then the variation propagation will fail.


Things that can go wrong (and they will)

Now that you hopefully have at least a basic understanding of what variations are and how they work we can begin to discuss where things fall apart (and if your deployment is anything more than demoware then you most likely will run into one or all of these issues).  The following issues are what I've personally encountered:

  1. The Relationships List can get "confused" and have either missing or invalid entries
  2. The page fields linking the page to the Relationships List can become invalid
  3. Web parts must inherit from Microsoft.SharePoint.WebPartPages.WebPart or the variation propagation will fail causing the application pool to crash (there is a hotfix for this: http://support.microsoft.com/kb/948947)
  4. Your pages must have a valid contact or no contact or you will not be able to get into the page settings for the page (this is more of a content deployment problem and not a variation problem)
  5. Your pages must have a valid page layout URL or the variation propagation will fail causing the application pool to crash (among other things)
  6. Content types for pages must match
  7. The variation label menu will only work with variation pages and not system pages (such as a list view page)
  8. The variation label menu will not preserve querystring values
  9. If you're working with large sites (several thousand pages and multiple variations) expect propagation times in terms of days (I've not personally dealt with this one but in my small environment with only a few pages it takes close to an hour for the pages to propagate so it's easy to extrapolate out)
  10. Navigation structure is not preserved from one variation to another
  11. Root site pages and other content is still available if I know how to get to it
  12. Custom list definitions and site definitions will need to be localized (use resource files) - definitions reverse engineered using Solution Generator will not be localized by default
  13. Values in localized choice columns will lose localization settings on propagation
  14. Only pages in the "Pages" library will be propagated - list items will have to be handled using custom event receivers or workflows
  15. Page field values are overwritten when a page is propagated

In the following sections I will try to cover each of the above issues and what I did to work around the issue.

1,2 - Relationships List Gets "Confused" and/or Page Fields are Invalid or Missing

There are times when the items in the Relationships List can get out of sync with the pages they are supposed to correspond to or vice-a-versa (either entries could be missing or IDs can be incorrect or mismatched).  When this happens you will run into issues with the propagation of page changes to variations.  For example, you may receive errors stating that a page already exists when you go to propagate changes to a page.  The most common way for this list to get corrupted is when pages are imported from other environments.  To fix the data in the Relationships List and the page fields I created a new stsadm command called "gl-fixvariationrelationships".  The command essentially loops through all the pages in a variation and tries to find the page with the matching name in the other variations and then rebuilds the data in the Relationships List and the page fields based on its findings.  Here's an example of how to run this command:

c:\>stsadm -o gl-fixvariationrelationships -url http://portal -verbose

Update 4/14/2008: I've just finished documenting this command here.

Update 9/2/2008: Tim Dobrinski has a great tool that he's put together for fixing many issues with the relationships list (including addressing sub-sites which I'm not currently dealing with).  You can find details about the tool here: http://www.thesug.org/blogs/lsuslinky/Lists/Posts/Post.aspx?List=ee6ea231%2D5770%2D4c2d%2Da99c%2Dc7c6e5fec1a7&ID=21

3 - Web Parts Must Inherit From Microsoft.SharePoint.WebPartPages.WebPart

I consider this particular issue to be a bug with the product because it could have been easily solved.  To demonstrate this I created a simple HelloWorld web part which just output some text and inherited from System.Web.UI.WebControls.WebPart.  I added this web part to my default.aspx page and published the page.  The result is that an InvalidCastException is thrown and the application pool crashes!  You can see the error details below - note that you'll see three errors in the event log all saying essentially the same thing.

image

If I change my web part code to inherit from Microsoft.SharePoint.WebPartPages.WebPart then the variation propagation works just fine.  So for your custom web parts the solution is real easy - just inherit from the SharePoint WebPart base class.  For third party web parts (the Dundas Chart web parts are a good example of this) you will need to wrap them in a custom web part.  The interesting thing to note is that even though the propagation seemed to fail and the application pool crashed I found that in most cases the web part was in fact successfully propagated.  The reason for this is because the failure happens in the PublishingPage object's FixWebPartUrlsForVariation method.  When this method is called the page and web parts have already been propagated and now the API is simply repairing some URLs within the web parts on the page - so in many cases repairs won't be necessary so everything will work just fine.  Microsoft could avoid this bug by simply doing a type check in the FixWebPartUrlsForVariation method before doing the cast.

Note that the only web parts out-of-the-box that are affected by the FixWebPartUrlsForVariation method are those that inherit from IWebPartVariationUpdate which at present only include the ContentByQueryWebPart and the TableOfContentsWebPart.

Update 4/21/2008: There is a hotfix for this issue - http://support.microsoft.com/kb/948947 - thanks to djeeg for adding the comment about this!

4 - Pages Must Have a Valid Contact

There are many ways in which the contact for a page can become invalid.  The most common is when a contact is deleted from the site collection.  Another way is when a page is imported from one environment to another.  Having an invalid contact "shouldn't" prevent the page variation propagation from occurring, so as far as variations are concerned this shouldn't be an issue.  However, many shops will choose to have a separate authoring environment and will import the pages into their public environment.  In this scenario you are very likely to end up with invalid contacts on your pages.  The issue comes when you choose to view the "Page Settings and Schedules" page.  If your page has an invalid contact associated with it you will receive a user not found error.  Of course the real problem here is that this is the page that allows you to fix the contact so your essentially stuck with no way of fixing the contact.  This seems like an obvious bug - the settings page should be able to handle the case of a contact being deleted so that you can assign a valid contact.

To work around these issues I've created another new custom stsadm command which I called "gl-fixpagecontact".  This command will identify all pages with an invalid contact and will either attempt to locate a valid contact, assign the current user as the contact, or assign a passed in user as the contact.  The contact field for the page is stored as a string in the format of "id;#name".  Often times when a page is imported from another environment the name will match but the ID will be different.  In this case the command will attempt to locate a principle with a matching name.

Here's an example of how to run this command:

C:\>stsadm -o gl-fixpagecontact -scope site -url http://demo -verbose

5 - Pages Must Have a Valid Page Layout URL

This particular issue can come up in a variety of situations and isn't necessarily limited to variations.  There are two ways in which this problem typically manifests itself - the first is when a page is migrated from another site (the entire site may have also been migrated) and the second is when you assign a page layout that exists in your source variation but not in your target variation.  To demonstrate the problem I created a new page layout which was just a copy of the WelcomeLinks.aspx page and stored it in the master page gallery located at http://demo/en/_catalogs/masterpage/forms/allitems.aspx - I then changed the /en/default.aspx page to use this new page layout.  After approving the page I clicked the "Update Variations" menu item to propagate my page.  The result is that 3 error events were logged in the event log and the application pool crashed!

To resolve these issues make sure that you either use only site collection level page layouts or that for every page layout you have in your variation sites you make sure that there is a layout with the same name in every other variation site (the contents can be different - they just need to be named the same).  In some cases (particularly when you import the content from another source) you won't be able to reset the page layout using the browser (going to the "Page Settings and Schedules" page will throw a FileNotFoundException).  To help fix this problem you can use a command I created a while ago called "gl-fixpublishingpagespagelayouturl".  This command has a variety of different options but here's a command of how to explicitly set the layout for a specific page:

C:\>stsadm -o gl-fixpublishingpagespagelayouturl -url http://demo/en -pagename default.aspx -pagelayout "http://demo/_catalogs/masterpage/WelcomeLinks.aspx, http://demo/_catalogs/masterpage/WelcomeLinks.aspx" -verbose -scope page

6 - Page Content Types Must Match

I've not experienced this particular issue but I thought it was worth mentioning here for completeness.  Jeremy Jameson gives a great explanation of the problem in his 3 part posts "Dumping MOSS 2007 Variations" so I won't bother re-iterating it here (the gist of it though - use SP1 with custom site definitions if you need to use page content types).

7 - Variations Label Menu Only Works with Publishing Pages

This is rather minor limitation but its one you should be aware of before you decided to use it.  If you don't allow any access to system or list pages (/_layouts/viewlsts.aspx or /Documents/Forms/AllItems.aspx for example) then you don't need to worry about this.  If you do and you have matching pages/lists throughout all your variations then you'll need to create a custom data source or your own menu if you want the ability to jump between variations.

8 - Variation Label Menu Does Not Preserve Querystring Values

I consider this to be a minor issue as well.  If you have a page which, for example, uses a page field filter which takes in values from the querystring and you then use the variation label menu to jump between variations those querystring values will not be preserved.  The reason I consider this minor is because I personally consider this menu to be nothing but a waste.  If you're asked to use it you really should ask yourself why?  Is it for the end users so that they can read the same content in different languages (who does that?)? Or is it for your testers so that they can jump between the various variations easily?  I think you'll find that this is a feature that, under most circumstances, will never be used by your end users - so why deploy it?

9 - Long Running Propagation Jobs with Large Sites

This is another one that I don't have personal experience with but Jeremy Jameson explains the problem real well in his previously mentioned post "Dumping MOSS 2007 Variations".

10 - Navigation Structure Doesn't Propagate Between Variations

This isn't actually a bug - it's by design and I completely understand the reasoning behind it.  You may have very different navigation needs between different languages and there's no easy way for the Microsoft to anticipate all needs.  However, most will want the structure of their global and current navigation to match across all variations.  This just another one of those things that you need to anticipate and plan for as you plan out your multilingual sites.  One thing that I've used to help keep things in sync is my "gl-enumnavigation" command to export the navigation from the source variation and then let the translators translate the resultant XML and then use my "gl-setnavigationnodes" command to set the navigation for the target variations (at least for an initial site propagation - after that I've just made the changes manually).

11 - Root default.aspx and Other Root Pages Are Still Available

One thing I found that needs some clarification in the Building Multilingual Solutions Using 2007 SharePoint Products and Technologies white paper is its description of the VariationRoot.aspx file:

"After a variation hierarchy is created, the Variations feature replaces the default page of the variation’s home site with a special page called VariationRoot.aspx"

To be more precise - the feature changes the Welcome Page settings so that VariationRoot.aspx is now the welcome page of the site - so if I type in "http://demo/" I'll be taken to the variation that's appropriate based on my browser settings.  However, if I type in "http://demo/pages/default.aspx" I'll be taken to the root site collections default.aspx page, not the variation site.  The same goes for any other content, lists, and sites that exist at the root site (assuming I have access).  The solution here is pretty simple - if you don't want them getting to it either delete it (if it's not needed) or lock it down so they can't get access to it.  In the case of the default.aspx page you may want to keep it there and keep the access open and then just add a redirection web part or some simple javascript to handle redirecting to the VariationRoot.aspx page.

12 - Custom List Definitions Should be Localized

If you're creating custom list definitions then you should plan on making sure those list definitions are localized - set display names, choice values, query parameters, etc., to resource strings.  One thing to be aware of is that if you use Solution Generator (part of the Visual Studio Extensions for WSS) to reverse engineer your lists you'll have to set all the fields to use localized strings which could add some time to your development/QA cycle.

13 - Localized Choice Columns Fail on Propagation

If you use any localized list definitions, as mentioned above, be aware that a side effect is that choice column values will be stored using the actual values, not the resource strings.  Sjoert Ebben discusses this issue in his post "5 reasons why you should not use variations".  Sjoert recommends a couple of solutions, one of which is to create a custom field type which will store the resource string and not the actual value.

14 - Only Pages in the Pages Library will be Propagated to Variations

To some this may seem like a pretty obvious point but I found that many I spoke to about using variations initially were rather surprised that they couldn't enable this functionality for any list.  In order to get list values (such as announcements and such) propagated between variations I enabled moderation on the desired lists and created a custom event receiver which, upon approval, copies the list item to the lists with the same name in all variations.  You could also use a custom workflow to do the same - it really just depends on how complex your needs are.  You'll find that handling modifications to data will require careful consideration.  I'm hoping to provide a sample of my event receiver in a follow-up post so be on the lookout for that.

15 - Page Field Values Are Overwritten Upon Propagation

So here's the dilemma - best practice with publishing pages is to use page fields for all content because web part changes are not stored with the history of the page - unfortunately though, when you are using a page field and you approve your page in the source variation the page will propagate to all target variations and all page field values will be overwritten.  One solution to this is to add a custom page field where the editor can indicate whether the content change is a minor modification or a new version and then create a custom workflow which will delete the draft in the variation if the page is a minor modification.  Some solutions will decide to simply put all content in web parts and just turn off the web part propagation - I still don't recommend this as you will not be able to roll back to a previous version of the page (again, web part changes are stored separately from page data and cannot be rolled back!).

Conclusion

Variations can be a very cool feature that, for simple sites, could give you exactly what you need to get you up and running with a multilingual site.  The key with variations, as with just about any SharePoint feature, is careful planning.  Make sure you give yourself plenty of time to prototype your solution and, most importantly, get your translators involved early on as you will find that many things will work just fine when everything is in English but as soon as you start translating your content and resource files you will encounter issues.  Sometimes these issues can be addressed with simple planning and change control policies but in most cases you'll be looking at custom code and/or workflows and/or site and/or list definitions.

So, if you're going to be working with variations make sure you've got lots of coffee, plenty of headache drugs, a good punching bag, and lots of time to switch to a backup plan ;)

2Oct/0728

Set Web Part State

As part of my upgrade I needed to be able to delete some web parts (closed or open) and close and open some web parts. In many respects this command, gl-setwebpartstate, is basically the twin to the gl-movewebpart command which I recently posted about. In fact, I created that command as a template for this command - the only difference from a code standpoint is that instead of calling MoveWebPart I'm calling either DeleteWebPart, CloseWebPart, or OpenWebPart (based on a user provided parameter). Everything else is exactly the same so once I had the gl-movewebpart command done creating this command took me less than 5 minutes. Below is the bit of code that differs from the gl-movewebpart code (I won't show the rest as it's identical to the gl-movewebpart command):

   1: if (Params["delete"].UserTypedIn)
   2:  manager.DeleteWebPart(wp);
   3: else if (Params["close"].UserTypedIn)
   4:  manager.CloseWebPart(wp);
   5: else if (Params["open"].UserTypedIn)
   6:  manager.OpenWebPart(wp);
   7:  
   8: if (!Params["delete"].UserTypedIn)
   9:  manager.SaveChanges(wp);

The syntax of the command I created can be seen below.

C:\>stsadm -help gl-setwebpartstate

stsadm -o gl-setwebpartstate


Opens, Closes, Adds, or Deletes a web part on a page.

Parameters:
        -url <web part page URL>
        {-id <web part ID> |
         -title <web part title>}
        {-delete |
         -open |
         -close |
         -add}
        {[-assembly <assembly name>]
         [-typename <type name>] |
         [-webpartfile <web part file if adding>]}
        [-zone <zone ID>]
        [-zoneindex <zone index>]
        {[-properties <comma separated list of key value pairs: "Prop1=Val1,Prop2=Val2">] |
         [-propertiesfile <path to a file with xml property settings (<Properties><Property Name="Name1">Value1</Property><Property Name="Name2">Value2</Property></Properties>)>]}
        [-publish]

Here’s an example of how to close a web part on a given page:

stsadm -o gl-setwebpartstate -url "http://intranet/hr/pages/default.aspx" -title "Grouped Listings" -close -publish

Update 12/10/2007: I've updated this command to now support the adding of web parts to a page. In doing this I also added the ability to set the zone and zone ID of a web part (thus encapsulating the gl-movewebpart command - I needed this in order to know where to add the part to so I figured I'd just allow the user to provide the same information for any other changes). I also added the ability to set properties of the web part. This is done using a comma separated list of key value pairs for simple data or an XML file with any encoded data. Note that only primitive data types are supported so if you try to set a property that requires a complex data type it will error out. Also - if you only wish to set the properties of an existing web part then simply pass in a command that matches the current state of the web part (so if the web part is open already then use the "-open" parameter and then pass in any desired properties).

Here's an example of how to add a web part to a page using simple properties:

stsadm -o gl-setwebpartstate -url "http://intranet/testweb1/default.aspx" -title "Table of Contents" -add -assembly "Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" -typename "Microsoft.SharePoint.Publishing.WebControls.TableOfContentsWebPart" -zone "Left" -zoneindex 0 -properties "ShowPages=false,LevelsToShow=3" -publish

Here's an example of how to add a web part to a page using an XML file containing properties:

stsadm -o gl-setwebpartstate -url "http://teamsites/pages/default.aspx" -title "I need to..." -add -assembly "Microsoft.SharePoint.Portal, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" -typename "Microsoft.SharePoint.Portal.WebControls.TasksAndToolsWebPart" -zone "MiddleRightZone" -zoneindex 0 -propertiesfile "c:\webpartprops.xml" -publish

The webpartprops.xml file will look like this:

<Properties> <Property Name="TasksAndToolsWebUrl">/SiteDirectory</Property> <Property Name="FilterFieldValue">Top Tasks</Property> <Property Name="FilterCategory">TasksAndTools</Property> <Property Name="TasksAndToolsListName">Sites</Property> <Property Name="Xsl">&lt;xsl:stylesheet xmlns:x=&quot;http://www.w3.org/2001/XMLSchema&quot; version=&quot;1.0&quot; exclude-result-prefixes=&quot;xsl ddwrt slwrt msxsl&quot; xmlns:ddwrt=&quot;http://schemas.microsoft.com/WebParts/v2/DataView/runtime&quot; xmlns:slwrt=&quot;http://schemas.microsoft.com/WebParts/v3/SummaryLink/runtime&quot; xmlns:xsl=&quot;http://www.w3.org/1999/XSL/Transform&quot; xmlns:msxsl=&quot;urn:schemas-microsoft-com:xslt&quot; xmlns:tnt=&quot;urn:schemas-microsoft-com:sharepoint:TasksAndToolsWebPart&quot; &gt; &lt;xsl:param name=&quot;tasksAndtools_IsRTL&quot; /&gt; &lt;xsl:param name=&quot;tasksAndTools_Width&quot; /&gt; &lt;xsl:template match=&quot;/&quot;&gt; &lt;xsl:call-template name=&quot;MainTemplate&quot;/&gt; &lt;/xsl:template&gt; &lt;xsl:template name=&quot;MainTemplate&quot; xmlns:ddwrt=&quot;http://schemas.microsoft.com/WebParts/v2/DataView/runtime&quot; xmlns:xsl=&quot;http://www.w3.org/1999/XSL/Transform&quot; xmlns:msxsl=&quot;urn:schemas-microsoft-com:xslt&quot;&gt; &lt;xsl:variable name=&quot;Rows&quot; select=&quot;/dsQueryResponse/NewDataSet/Row&quot;/&gt; &lt;xsl:variable name=&quot;RowCount&quot; select=&quot;count($Rows)&quot;/&gt; &lt;table border=&quot;0&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse:collapse; margin:0px;&quot;&gt; &lt;tr style=&quot;margin-top:3px;margin-bottom:1px;height:28px;border:0px;&quot;&gt; &lt;td style=&quot;padding-left:4px; white-space:nowrap ;&quot;&gt; &lt;xsl:if test=&quot;string-length($tasksAndTools_Width) != 0&quot;&gt; &lt;select id=&quot;TasksAndToolsDDID&quot; class=&quot;ms-selwidth&quot; style=&quot;width:{$tasksAndTools_Width}&quot; size=&quot;1&quot; title=&quot;Choose a task that you need to perform&quot; &gt; &lt;option selected=&quot;true&quot; value=&quot;0&quot;&gt;Choose task&lt;/option&gt; &lt;xsl:call-template name=&quot;MainTemplate.body&quot;&gt; &lt;xsl:with-param name=&quot;Rows&quot; select=&quot;$Rows&quot;/&gt; &lt;/xsl:call-template&gt; &lt;/select&gt; &lt;/xsl:if&gt; &lt;xsl:if test=&quot;string-length($tasksAndTools_Width) = 0&quot;&gt; &lt;select id=&quot;TasksAndToolsDDID&quot; class=&quot;ms-selwidth&quot; size=&quot;1&quot; title=&quot;Choose a task that you need to perform&quot;&gt; &lt;option selected=&quot;true&quot; value=&quot;0&quot;&gt;Choose task&lt;/option&gt; &lt;xsl:call-template name=&quot;MainTemplate.body&quot;&gt; &lt;xsl:with-param name=&quot;Rows&quot; select=&quot;$Rows&quot;/&gt; &lt;/xsl:call-template&gt; &lt;/select&gt; &lt;/xsl:if&gt; &lt;/td&gt; &lt;xsl:if test=&quot;$tasksAndtools_IsRTL = true()&quot;&gt; &lt;td style=&quot;padding-right:5px; padding-left: 14px;white-space:nowrap ;&quot;&gt; &lt;a id=&quot;TasksAndToolsGo&quot; accesskey=&quot;G&quot; title=&quot;Go&quot; href=&quot;javascript:TATWP_jumpMenu()&quot;&gt; &lt;img title=&quot;Go&quot; alt=&quot;Go&quot; border=&quot;0&quot; src=&quot;/_layouts/images/icongo01RTL.gif&quot; style=&quot;border-width:0px;&quot; onmouseover=&quot;this.src='/_layouts/images/icongo02RTL.gif'&quot; onmouseout=&quot;this.src='/_layouts/images/icongo01RTL.gif'&quot;/&gt; &lt;/a&gt; &lt;/td&gt; &lt;/xsl:if&gt; &lt;xsl:if test=&quot;$tasksAndtools_IsRTL = false()&quot;&gt; &lt;td style=&quot;padding-right:14px; padding-left: 5px;white-space:nowrap ;&quot;&gt; &lt;a id=&quot;TasksAndToolsGo&quot; accesskey=&quot;G&quot; title=&quot;Go&quot; href=&quot;javascript:TATWP_jumpMenu()&quot;&gt; &lt;img title=&quot;Go&quot; alt=&quot;Go&quot; border=&quot;0&quot; src=&quot;/_layouts/images/icongo01.gif&quot; style=&quot;border-width:0px;&quot; onmouseover=&quot;this.src='/_layouts/images/icongo02.gif'&quot; onmouseout=&quot;this.src='/_layouts/images/icongo01.gif'&quot; /&gt; &lt;/a&gt; &lt;/td&gt; &lt;/xsl:if&gt; &lt;/tr&gt; &lt;/table&gt; &lt;/xsl:template&gt; &lt;xsl:template name=&quot;MainTemplate.body&quot; xmlns:ddwrt=&quot;http://schemas.microsoft.com/WebParts/v2/DataView/runtime&quot; xmlns:xsl=&quot;http://www.w3.org/1999/XSL/Transform&quot; xmlns:msxsl=&quot;urn:schemas-microsoft-com:xslt&quot;&gt; &lt;xsl:param name=&quot;Rows&quot;/&gt; &lt;xsl:for-each select=&quot;$Rows&quot;&gt; &lt;xsl:variable name=&quot;GroupStyle&quot; select=&quot;'auto'&quot;/&gt; &lt;option style=&quot;display:{$GroupStyle}&quot; value=&quot;{ddwrt:EnsureAllowedProtocol(substring-before(@URL, ', '))}&quot; &gt; &lt;xsl:value-of select=&quot;@Title&quot;/&gt; &lt;/option&gt; &lt;/xsl:for-each&gt; &lt;/xsl:template&gt; &lt;/xsl:stylesheet&gt;</Property> </Properties>

Update 7/8/2008: I've added a new parameter, -webpartfile, which can be used to effectively import an exported web part file.  Just use it in conjunction with the -add option (do not use the -assembly or -typename parameters).  I also adjusted the code so that if it fails to add the web part using the object model it will revert to the web service - this is to account for some web parts (like the KPI web part) that require a valid SPContext object.

2Oct/077

Move a Web Part on a Page

This particular command was one that I didn't actually need but rather I needed a starting place for a couple other commands that I did need (to be documented) and moving a web part seemed a simple enough starting place for manipulating web parts. So this one was really just a template for my other commands but it works rather well and who knows, maybe someone will benefit from it. Moving a web part is actually quite simple - you just call the SPLimitedWebPartManager's MoveWebPart method passing in the web part to move, the zone ID to move to, and the zone index (position in the zone). And then you just call SaveChanges passing in the web part.

All the real work is in locating the web part to move which is a little trickier than it should be because the Title field is not unique but it's a lot more convenient for the user to use the title rather than the cryptic ID. The other thing is the the zone ID is a string and could theoretically be any value so the caller needs to know what values to provide (this is where the gl-enumpagewebparts command comes in handy along with retrieving the web parts ID if necessary). To facilitate getting the web part I created a couple helper methods, one gets the web part by ID the other by Title:

   1: /// <summary>
   2: /// Gets the web part by id.
   3: /// </summary>
   4: /// <param name="web">The web.</param>
   5: /// <param name="url">The URL.</param>
   6: /// <param name="id">The id.</param>
   7: /// <param name="manager">The web part manager.</param>
   8: /// <returns></returns>
   9: internal static WebPart GetWebPartById(SPWeb web, string url, string id, out SPLimitedWebPartManager manager)
  10: {
  11:  manager = web.GetLimitedWebPartManager(url, PersonalizationScope.Shared);
  12:  WebPart wp = manager.WebParts[id];
  13:  if (wp == null)
  14:  {
  15:   manager = web.GetLimitedWebPartManager(url, PersonalizationScope.User);
  16:   wp = manager.WebParts[id];
  17:  }
  18:  return wp;
  19: }
  20:  
  21: /// <summary>
  22: /// Gets the web part by title.
  23: /// </summary>
  24: /// <param name="web">The web.</param>
  25: /// <param name="url">The URL.</param>
  26: /// <param name="title">The title.</param>
  27: /// <param name="manager">The web part manager.</param>
  28: /// <returns></returns>
  29: internal static WebPart GetWebPartByTitle(SPWeb web, string url, string title, out SPLimitedWebPartManager manager)
  30: {
  31:  manager = web.GetLimitedWebPartManager(url, PersonalizationScope.Shared);
  32:  List<WebPart> foundParts = new List<WebPart>();
  33:  WebPart wp = null;
  34:  foreach (WebPart tempWP in manager.WebParts)
  35:  {
  36:   if (tempWP.DisplayTitle.ToLowerInvariant() == title.ToLowerInvariant())
  37:   {
  38:    foundParts.Add(tempWP);
  39:    wp = tempWP;
  40:   }
  41:  }
  42:  if (foundParts.Count == 0)
  43:  {
  44:   manager = web.GetLimitedWebPartManager(url, PersonalizationScope.User);
  45:   foreach (WebPart tempWP in manager.WebParts)
  46:   {
  47:    if (tempWP.DisplayTitle.ToLowerInvariant() == title.ToLowerInvariant())
  48:    {
  49:     foundParts.Add(tempWP);
  50:     wp = tempWP;
  51:    }
  52:   }
  53:  }
  54:  if (foundParts.Count > 1)
  55:  {
  56:   string msg = "Found more than one web part matching the specified title.  Use the ID instead:\r\n\r\n";
  57:   XmlDocument xmlDoc = new XmlDocument();
  58:   string tempXml = null;
  59:   foreach (WebPart tempWP in foundParts)
  60:   {
  61:    tempXml += EnumPageWebParts.GetWebPartDetailsMinimal(tempWP, manager);
  62:   }
  63:   xmlDoc.LoadXml("<MatchingWebParts>" + tempXml + "</MatchingWebParts>");
  64:   throw new SPException(msg + GetFormattedXml(xmlDoc));
  65:  }
  66:  return wp;
  67: }

Once you've got the web part the rest is fairly easy - just need to provide a bit of logic to figure out which method to call and then handle the check in/out of the web part page so that the move operation can work:

   1: public override int Run(string command, StringDictionary keyValues, out string output)
   2: {
   3:  output = string.Empty;
   4:  
   5:  InitParameters(keyValues);
   6:  
   7:  SPBinaryParameterValidator.Validate("id", Params["id"].Value, "title", Params["title"].Value);
   8:  if (!Params["zone"].UserTypedIn && !Params["zoneindex"].UserTypedIn)
   9:   throw new SPSyntaxException("You must specify at least the zone or zoneindex parameters.");
  10:  
  11:  string url = Params["url"].Value;
  12:  
  13:  using (SPSite site = new SPSite(url))
  14:  using (SPWeb web = site.OpenWeb()) // The url contains a filename so AllWebs[] will not work unless we want to try and parse which we don't
  15:  {
  16:   SPFile file = web.GetFile(url);
  17:  
  18:   if (!Utilities.IsCheckedOut(file.Item) || !Utilities.IsCheckedOutByCurrentUser(file.Item))
  19:    file.CheckOut(); // If it's checked out by another user then this will throw an informative exception so let it do so.
  20:  
  21:   string displayTitle = string.Empty;
  22:   try
  23:   {
  24:    WebPart wp;
  25:    SPLimitedWebPartManager manager;
  26:    if (Params["id"].UserTypedIn)
  27:    {
  28:     wp = Utilities.GetWebPartById(web, url, Params["id"].Value, out manager);
  29:    }
  30:    else
  31:    {
  32:     wp = Utilities.GetWebPartByTitle(web, url, Params["title"].Value, out manager);
  33:     if (wp == null)
  34:     {
  35:      throw new SPException(
  36:       "Unable to find specified web part.  Try specifying the -id parameter instead (use enumpagewebparts to get the ID)");
  37:     }
  38:    }
  39:  
  40:    if (wp == null)
  41:    {
  42:     throw new SPException("Unable to find specified web part.");
  43:    }
  44:  
  45:    string zoneID = manager.GetZoneID(wp);
  46:    int zoneIndex = wp.ZoneIndex;
  47:  
  48:    if (Params["zone"].UserTypedIn)
  49:     zoneID = Params["zone"].Value;
  50:    if (Params["zoneindex"].UserTypedIn)
  51:     zoneIndex = int.Parse(Params["zoneindex"].Value);
  52:  
  53:    // Set this so that we can add it to the check-in comment.
  54:    displayTitle = wp.DisplayTitle;
  55:  
  56:    manager.MoveWebPart(wp, zoneID, zoneIndex);
  57:    manager.SaveChanges(wp);
  58:   }
  59:   finally
  60:   {
  61:    file.CheckIn("Checking in changes to page layout due to moving of web part " + displayTitle);
  62:  
  63:    if (Params["publish"].UserTypedIn)
  64:     file.Publish("Publishing changes to page layout due to moving of web part " + displayTitle);
  65:   }
  66:  }
  67:  
  68:  return 1;
  69: }
The syntax of the command I created can be seen below.

C:\>stsadm -help gl-movewebpart

stsadm -o gl-movewebpart

Moves a web part on a page.

Parameters:
        -url <web part page URL>
        {-id <web part ID> |
         -title <web part title>}
        [-zone <zone ID>]
        [-zoneindex <zone index>]
        [-publish]

Here's an example of how to move a web part to a different zone on a page:

stsadm -o gl-movewebpart -url "http://intranet/hr/pages/default.aspx" -title "Grouped Listings" -zone MiddleLeftZone -zoneindex 1 -publish

2Oct/077

Enumerate Page Web Parts

As part of the upgrade I needed to be able to fix some web parts that did not migrate correctly (either during the upgrade itself or as a result of moving a web). Before I started messing around with the web parts though I wanted to be able to see what I was dealing with. So I decided to create this simple command called gl-enumpagewebparts that would enable me to list out in XML all the web parts that are on a given page (open or closed).

One thing that I found that was very interesting was that the web part manager export method treats V2 and V3 web parts very differently. But perhaps the biggest annoyance I found was that I couldn't get the web part zone from the web part instance itself - I had to use the web part manager (SPLimitedWebPartManager) to get it (took me longer than I'd like to admit to figure that out). This command is really quite simple - it takes in an url to a web part page, loads up an SPLimitedWebPartManager (for both the shared and personal views) and then loops through the WebParts collection outputting the results as XML.

I created three separate methods to get the XML details - one is verbose and essentially just uses the built in Export() method to get the XML (you can get these results via the -verbose parameter), another is a bit simpler and is constructed by hand (this is the default) and a third is actually for use by another command that I created which I'll be documenting soon. The core code is shown below:

   1: /// <summary>
   2: /// Runs the specified command.
   3: /// </summary>
   4: /// <param name="command">The command.</param>
   5: /// <param name="keyValues">The key values.</param>
   6: /// <param name="output">The output.</param>
   7: /// <returns></returns>
   8: public override int Run(string command, StringDictionary keyValues, out string output)
   9: {
  10:  output = string.Empty;
  11:  
  12:  InitParameters(keyValues);
  13:  
  14:  string url = Params["url"].Value;
  15:  
  16:  XmlDocument xmlDoc = new XmlDocument();
  17:  xmlDoc.AppendChild(xmlDoc.CreateElement("WebParts"));
  18:  xmlDoc.DocumentElement.SetAttribute("page", url);
  19:  
  20:  using (SPSite site = new SPSite(url))
  21:  using (SPWeb web = site.OpenWeb()) // The url contains a filename so AllWebs[] will not work unless we want to try and parse which we don't
  22:  {
  23:   XmlElement shared = xmlDoc.CreateElement("Shared");
  24:   xmlDoc.DocumentElement.AppendChild(shared);
  25:  
  26:   SPLimitedWebPartManager webPartMngr = web.GetLimitedWebPartManager(url, PersonalizationScope.Shared);
  27:   
  28:   string tempXml = string.Empty;
  29:   foreach (WebPart wp in webPartMngr.WebParts)
  30:   {
  31:    if (Params["verbose"].UserTypedIn)
  32:     tempXml += GetWebPartDetails(wp, webPartMngr);
  33:    else
  34:     tempXml += GetWebPartDetailsSimple(wp, webPartMngr);
  35:   }
  36:   shared.InnerXml = tempXml;
  37:  
  38:   XmlElement user = xmlDoc.CreateElement("User");
  39:   xmlDoc.DocumentElement.AppendChild(user);
  40:   
  41:   webPartMngr = web.GetLimitedWebPartManager(url, PersonalizationScope.User);
  42:   tempXml = string.Empty;
  43:   foreach (WebPart wp in webPartMngr.WebParts)
  44:   {
  45:    if (Params["verbose"].UserTypedIn)
  46:     tempXml += GetWebPartDetails(wp, webPartMngr);
  47:    else
  48:     tempXml += GetWebPartDetailsSimple(wp, webPartMngr);
  49:   }
  50:   user.InnerXml = tempXml;
  51:  
  52:  }
  53:  
  54:  output += Utilities.GetFormattedXml(xmlDoc);
  55:  
  56:  return 1;
  57: }
  58:  
  59: #endregion
  60:  
  61: /// <summary>
  62: /// Gets the web part details.
  63: /// </summary>
  64: /// <param name="wp">The web part.</param>
  65: /// <param name="manager">The web part manager.</param>
  66: /// <returns></returns>
  67: internal static string GetWebPartDetails(WebPart wp, SPLimitedWebPartManager manager)
  68: {
  69:  StringBuilder sb = new StringBuilder();
  70:  
  71:  XmlTextWriter xmlWriter = new XmlTextWriter(new StringWriter(sb));
  72:  xmlWriter.Formatting = Formatting.Indented;
  73:  manager.ExportWebPart(wp, xmlWriter);
  74:  xmlWriter.Flush();
  75:  
  76:  XmlDocument xmlDoc = new XmlDocument();
  77:  xmlDoc.LoadXml(sb.ToString());
  78:  
  79:  XmlElement elem = xmlDoc.DocumentElement;
  80:  if (xmlDoc.DocumentElement.Name == "webParts")
  81:  {
  82:   elem = (XmlElement)xmlDoc.DocumentElement.ChildNodes[0];
  83:   
  84:   // We've found a v3 web part but the export method does not export what the zone ID is so we
  85:   // have to manually add that in.  Unfortunately the Zone property is always null because we are
  86:   // using a SPLimitedWebPartManager so we have to use the helper method GetZoneID to set the zone ID.
  87:   XmlElement property = xmlDoc.CreateElement("property");
  88:   property.SetAttribute("name", "ZoneID");
  89:   property.SetAttribute("type", "string");
  90:   property.InnerText = manager.GetZoneID(wp);
  91:   elem.ChildNodes[1].ChildNodes[0].AppendChild(property);
  92:  }
  93:  
  94:  return elem.OuterXml.Replace(" xmlns=\"\"", ""); // Just some minor cleanup to deal with erroneous namespace tags added due to the zoneID being added manually.
  95: }

The syntax of the command I created can be seen below.

C:\>stsadm -help gl-enumpagewebparts

stsadm -o gl-enumpagewebparts

Lists all the web parts that have been added to the specified page.

Parameters:
        -url <web part page URL>
        [-verbose]

Here’s an example of how to list all the web parts on a given page and dump to a text file:

stsadm -o gl-enumpagewebparts -url "http://intranet/hr/pages/default.aspx" -verbose > webparts.xml

4Sep/07250

Convert a Sub-site to a Site Collection

I finally figured it out! This was supposed to be one of those very simple tasks that I should have been able to do without any custom code. Turning a sub-site (or web) into a site collection (or top level site) turned out to be the most difficult task I've yet to face with SharePoint 2007. In theory you should be able to do this using the following commands which could be put into a batch file:

REM Create a test web for exporting
stsadm -o createweb -url "http://intranet/testweb" -sitetemplate "SPSTOPIC#0"

REM Export the test web to the filesystem
stsadm -o export -url "http://intranet/testweb" -filename "c:\testweb" -includeusersecurity -versions 4 -nofilecompression -quiet

REM Create a managed path for the new top level site
stsadm -o addpath -url "http://intranet/testsite" -type explicitinclusion

REM Create an empty site with a default site template (note that if you don't specify a template you have to manually activate the required features)
stsadm -o createsite -url "http://intranet/testsite" -owneremail "someone@example.com" -ownerlogin "domain\username" -sitetemplate "SPSTOPIC#0"

REM Import the site
stsadm -o import -url "http://intranet/testsite" -filename "c:\testweb" -includeusersecurity -nofilecompression -quiet

Unfortunately what you get is only a partially functional site (and in some case not functional at all). There are several errors that you are likely to encounter after running the above using the created testweb or your own existing web. The first and most obvious error is that when you load the default.aspx page of the new site you may get a File Not Found error (note that running the above as is will not give you this error). This is the result of the publishing pages PageLayout URL getting messed up (effectively still pointing to an old value). I addressed this specific issue with a separate command (http://blog.falchionconsulting.com/index.php/2007/08/fix-publishing-pages-page-layout-url/) and the I've encapsulated that functionality into the new commands I've created which are detailed below.

The next error you're likely to see is on the Area Template Settings page (Site Settings -> Page layouts and site templates). The specific error is "Data at the root level is invalid. Line 1, position 1".

Anyone who's done a lot of XML work should recognize this error as an XML parsing error. This error occurs if the web you imported from was set to inherit it's page layouts from it's parent. When a web is setup this way there's a property called "__PageLayouts" which gets set to "__inherit".

For a top level site collection this value should always be either an empty string (all page layouts are available) or XML describing which layouts are available. The import operation does not consider this and leaves the value as is thus resulting in the XML error when attempting to parse "__inherit" as XML. The fix for this is simple enough - change the value to an empty string. Unfortunately that's not all we have to do. Fixing the above error results in the page loading without errors, however, the page layouts section does not load. There's still several issues that need to be resolved. If you now go and view the master gallery (Site Settings -> Master pages and page layouts) you should see all the default page layouts. If you have any custom page layouts those won't exist and will cause problems.

Also, if you attempt to edit a file you'll notice that even though we used a publishing template it doesn't prompt you to check the file out. What's more is that once you view the form for a layout you should see that only the core fields are present (no Content Type, Associated Content Type, Variations, etc.).

There are several things that need to be fixed here - first we need to activate all the features that would otherwise be activated on a new site collection (need this so that we can get the publishing workflow options enabled). Second we need to reset all the properties for the gallery to match that of our source gallery (namely we need to allow management of content types).

Third we have to change the ContentType field from being a Text field to being a Choice field (more about this in a minute). Fourth we need to re-associate each file as a Page Layout file by setting all the necessary properties (Content Type, Associated Content Type, etc.). In regards to changing the ContentType field this is the one that caused me the most headache to figure out. For some reason during the import of the site this field gets a bit messed up (note that I'm not referring to the ContentType field that is linked in from the Page content type which is associated with the library but rather another field that is part of the gallery definition itself). The field should be a Choice field with options such as "Page Layout", "Publishing Mater Page", "Master Page", and "Folder". However, during the import the field is converted to a Text field - don't ask me why.

The result is that when you query for the list of available page layouts the unmanaged code that Microsoft uses to do the actual query chokes because it can't find a matching value in the list so it produces an invalid query which will always return no results back. To fix this I copy the source master page gallery on top of the target gallery using the content deployment API. I also found that (with SP2) the PublishingResources feature seemed to correct the issue (at least in the tests that I ran).

UPDATE 9/4/2007: I just discovered that another issue is related to the global navigation. If you view the navigation via the browser it will look as though everything is just peachy but if you attempt to manipulate the navigation programmatically you'll find that the PublishingWeb's GlobalNavigationNodes property is empty (no items are present). This is because the global navigation is stored at the site collection level so when you import the web it does not take the global navigation with it, just the current. The fix is simple enough - just loop through the current navigation collection and copy it to the global navigation collection. This will help to allow other programmatic manipulations of the global navigation to succeed.

UPDATE 7/6/2009:  I am now calling the code that I created to copy content types from one site collection to another.  This solves issues that can occur if a site collection content type was not created via a Feature.  I've also added additional logging to better show what is happening.

So, to summarize the things that need to be repaired after importing into your empty site collection:

  1. Activate any features that are needed by the site
  2. Set the master page gallery settings
    1. Enable content type management
    2. Set your publishing options
    3. Fix the Content Type field so that it's a Choice field type by copying the source master page gallery
  3. Copy missing content types from the source site collection.
  4. Fix the Page Layouts and Site Templates
    1. Set the "__PageLayouts" property to an empty string (can then be set to something else using SetAvailablePageLayouts() but first needs to be set to an empty string as SetAvailablePageLayouts() will not work until it's fixed)
    2. Copy any missing page layouts from the source
    3. Set all appropriate properties on each page layout
  5. Fix all the publishing pages PageLayout property to have the correct URL
  6. UPDATE 9/4/2007: Update the global navigation

Some of the above can be done via the browser but most of it requires programmatic changes. In order to solve all these problems I've created two custom stsadm commands - the first will take a site make all the repairs identified above (so it assumes you've already imported the site).

The second basically just abstracts the whole process of exporting a web, creating a site, importing into the site, and then repairing the site (this way the entire process can be done with just one command). The commands I created are detailed below (forgive the verbosity of the names - I had trouble coming up with something shorter).

1. gl-repairsitecollectionimportedfromsubsite

The code is fairly well documented so rather than discuss it all (there's a lot of it) I've linked it to this post here. The syntax of the command can be seen below:

C:\>stsadm -help gl-repairsitecollectionimportedfromsubsite

stsadm -o gl-repairsitecollectionimportedfromsubsite

Repairs a site collection that has been imported from an exported sub-site.  Note that the sourceurl can be the actual source site or any site collection that can be used as a model for the target.

Parameters:
        -sourceurl <source location of the existing sub-site or model site collection>
        -targeturl <target location for the new site collection>

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-repairsitecollectionimportedfromsubsite WSS 3, MOSS 2007 Released: 9/4/2007
Updated: 7/6/2009

Parameter Name Short Form Required Description Example Usage
sourceurl source Yes The URL to the source sub-site to convert. -sourceurl http://portal/subsite

-source http://portal/subsite
targeturl target Yes The URL of the new site collection to create. -targeturl http://portal/sites/site

-target http://portal/sites/site

Here’s an example of how to repair the site created using the batch file above:

stsadm –o gl-repairsitecollectionimportedfromsubsite –sourceurl "http://intranet/testweb/" -targeturl "http://intranet/testsite/"

2. gl-convertsubsitetositecollection

As stated above, this command is just an abstraction of other commands - it simply calls out to stsadm to do export the site (note that you can provide a previously exported site file/folder), create the managed path, create the empty site, import the site, and finally repair the imported site. As there's nothing spectacular going on here I didn't bother culling the code out in this post (download the project if you're interested in the details). The syntax of the command can be seen below:

C:\>stsadm -help gl-convertsubsitetositecollection

stsadm -o gl-convertsubsitetositecollection


Converts a sub-site to a top level site collection via a managed path.

Parameters:

        -sourceurl <source location of the existing sub-site or model site collection>
        -targeturl <target location for the new site collection>
        -owneremail <someone@example.com>
        [-createmanagedpath]
        [-haltonwarning]
        [-haltonfatalerror]
        [-includeusersecurity]
        [-suppressafterevents (disable the firing of "After" events when creating or modifying list items)]
        [-exportedfile <filename of exported site if previously exported>]
        [-nofilecompression]
        [-ownerlogin <DOMAIN\name>]
        [-ownername <display name>]
        [-secondaryemail <someone@example.com>]
        [-secondarylogin <DOMAIN\name>]
        [-secondaryname <display name>]
        [-lcid <language>]
        [-title <site title>]
        [-description <site description>]
        [-hostheaderwebapplicationurl <web application url>]
        [-quota <quota template>]
        [-deletesource]
        [-createsiteinnewdb]
        [-createsiteindb]
        [-databaseuser <database username>]
        [-databasepassword <database password>]
        [-databaseserver <database server name>]
        [-databasename <database name>]
        [-verbose]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-convertsubsitetositecollection WSS 3, MOSS 2007 Released: 9/4/2007
Updated: 7/6/2009

Parameter Name Short Form Required Description Example Usage
sourceurl source Yes The URL to the source sub-site to convert. -sourceurl http://portal/subsite

-source http://portal/subsite
targeturl target Yes The URL of the new site collection to create. -targeturl http://portal/sites/site

-target http://portal/sites/site
owneremail oe Yes

The site owner's e-mail address.  Must be valid e-mail address, in the form someone@example.com.

-owneremail someone@example.com

-oe someone@example.com
createmanagedpath createpath No Create a new managed path for the site collection. -createmanagedpath

-createpath
haltonwarning warning No Stop execution of the command if a warning event occurs during the export or import process. -haltonwarning

-warning
haltonfatalerror error No Stop execution of the command if a fatal error occurs during the export or import process. -haltonfatalerror

-error
exportedfile file No Use a previously exported site (created using stsadm's export command). -exportedfile c:\exportdata\site

-file c:\exportdata\site
nofilecompression   No Do not compress the site when exporting (or if previously exported use an uncompressed file for the import). -nofilecompression
ownerlogin ol

If your farm does not have Active Directory account creation mode enabled, then this parameter is required.

This parameter should not be provided if your farm has Active Directory account creation mode enabled, as Microsoft Office SharePoint Server 2007 will automatically create a site collection owner account in Active Directory based on the owner e-mail address.

The site owner's user account.  Must be a valid Windows user name, and must be qualified with a domain name, for example, domain\name

-ownerlogin domain\name

-ol domain\name
ownername on No

The site owner's display name.

-ownername "Gary Lapointe"

-on "Gary Lapointe"
secondaryemail se No

The secondary site owner's e-mail address.  Must be valid e-mail address, in the form someone@example.com.

-secondaryemail someone@example.com

-se someone@example.com
secondarylogin sl

If your farm does not have Active Directory account creation mode enabled, then this parameter is required.

This parameter should not be provided if your farm has Active Directory account creation mode enabled, as Microsoft Office SharePoint Server 2007 will automatically create a site collection owner account in Active Directory based on the owner e-mail address.

The secondary site owner's user account.  Must be a valid Windows user name, and must be qualified with a domain name, for example, domain\name

-secondarylogin domain\name

-sl domain\login
secondaryname sn No

The secondary site owner's display name.

-secondaryname "Pam Lapointe"

-sn "Pam Lapointe"
lcid   No

A valid locale ID, such as "1033" for English.  You must specify this parameter when using a non-English template.

-lcid 1033
title t No

The title of the new site collection (this value will be overwritten when the site is imported - it is available only to help in situations in which the import fails).

-title "New Site"
description desc No

Description of the site collection (this value will be overwritten when the site is imported - it is available only to help in situations in which the import fails).

-description "New Site Description"

-desc "New Site Description"
hostheaderwebapplicationurl hhurl No

A valid URL assigned to the Web application by using Alternate Access Mapping (AAM), such as "http://server_name".

When the hostheaderwebapplicationurl parameter is present, the value of the url parameter is the URL of the host-named site collection and value of the hostheaderwebapplicationurl parameter is the URL of the Web application that will hold the host-named site collection.

-hostheaderwebapplicationurl http://newsite

-hhurl http://newsite
quota   No

The quota template to apply to sites created on the virtual server.

-quota Portal
deletesource   No Delete the source site after conversion (only recommended if significant testing has occurred). -deletesource
createsiteinnewdb newdb No Create the site collection in a content database. -createsiteinnewdb

-newdb
createsiteindb db No Create the site collection in an existing content database. -createsiteindb

-db
databaseserver ds No The database server containing the specified content database.  If not specified then the default database server is used. -databaseserver spsql1

-ds spsql1
databaseuser du No

The administrator user name for the SQL Server database.

-databaseuser domain\user

-du domain\user
databasepassword dp No

The password that corresponds to the administrator user name for the SQL Server database.

-databasepassword password

-dp password
databasename dn Yes if createsiteinnewdb or createsiteindb is specified.

The name of the content database to put the site collection in (will be created if createsiteinnewdb is specified).

-databasename SharePoint_Content1

-db SharePoint_Content1
suppressafterevents sae No Disable the firing of After events when creating or modifying files or list items during the import. -suppressafterevents

-sae
verbose v No Displays logging information when executing. -verbose

-v

Here's an example of how to do all that the batch file above is doing (minus the creation of the testweb) as well as the repair operation all with one command:

stsadm –o gl-convertsubsitetositecollection –sourceurl "http://intranet/testweb/" -targeturl "http://intranet/testsite/" -createmanagedpath -nofilecompression -owneremail "someone@example.com" -ownerlogin "domain\user" -deletesource

One area of improvement may be to pull the owner and secondary owner information from the source site collection so that this information does not have to be provided - maybe I'll do that if I feel I have the time or if people express enough interest. Note that you can specify a title and description but they'll be overwritten during the import - I only included them so that if the import fails and you're left with an incomplete site you'll at least have a name for it if you should forget to delete it and stumble upon it a year later.

Figuring out how to solve all the issues surrounding converting a web to a site collection was a real pain the a$$ so any feedback that people have on this would be greatly appreciated - hopefully if there are others out there that have stumbled on this then they'll benefit from it as well. Keep in mind also that though I think I've solved all the errors related to the conversion it's possible that different implementations may have additional errors that I have not seen - if that's the case please let me know (especially if you've solved the problems) so that I can share with others.

Update 9/21/2007: I've fixed a couple minor bugs that pop up when converting a non-publishing site. I've also enhanced the command to take advantage of another new command I created: gl-updatev2tov3upgradeareaurlmappings (updates the url mapping of V2 bucket webs to V3 webs thereby reflecting the change of url as a result of the move so if a user tries to hit the V2 url it will redirect to the new and updated V3 url).

Update 10/2/2007: I've enhanced the command to take advantage of another new command I created: gl-retargetcontentquerywebpart (fixes Grouped Listings web parts that remained pointed at the old list rather than the newly imported list).

Update 10/12/2007: I've removed the retainobjectidentity parameter. If you attempt to use this parameter you will receive a syntax error. Turns out that retaining the object identity when going from a sub-site to a site collection just created a nightmare. However, because I still had to handle these web parts that were broken I decided to enhance the repair routines to manually retarget the DataFormWebPart and ContentByQueryWebPart web parts. So if a matching list can be found on the source then any of these web parts on your pages should be fixed so that you don't have to manually fix them (the gl-repairsitecollectionimportedfromsubsite command will do the same).

Update 7/6/2009: I removed the direct DB access code and added support for copying content types from the source.

29Aug/07129

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".

29Aug/073

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>