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"><xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema" version="1.0" exclude-result-prefixes="xsl ddwrt slwrt msxsl" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:slwrt="http://schemas.microsoft.com/WebParts/v3/SummaryLink/runtime" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:tnt="urn:schemas-microsoft-com:sharepoint:TasksAndToolsWebPart" > <xsl:param name="tasksAndtools_IsRTL" /> <xsl:param name="tasksAndTools_Width" /> <xsl:template match="/"> <xsl:call-template name="MainTemplate"/> </xsl:template> <xsl:template name="MainTemplate" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt"> <xsl:variable name="Rows" select="/dsQueryResponse/NewDataSet/Row"/> <xsl:variable name="RowCount" select="count($Rows)"/> <table border="0" cellpadding="0" cellspacing="0" style="border-collapse:collapse; margin:0px;"> <tr style="margin-top:3px;margin-bottom:1px;height:28px;border:0px;"> <td style="padding-left:4px; white-space:nowrap ;"> <xsl:if test="string-length($tasksAndTools_Width) != 0"> <select id="TasksAndToolsDDID" class="ms-selwidth" style="width:{$tasksAndTools_Width}" size="1" title="Choose a task that you need to perform" > <option selected="true" value="0">Choose task</option> <xsl:call-template name="MainTemplate.body"> <xsl:with-param name="Rows" select="$Rows"/> </xsl:call-template> </select> </xsl:if> <xsl:if test="string-length($tasksAndTools_Width) = 0"> <select id="TasksAndToolsDDID" class="ms-selwidth" size="1" title="Choose a task that you need to perform"> <option selected="true" value="0">Choose task</option> <xsl:call-template name="MainTemplate.body"> <xsl:with-param name="Rows" select="$Rows"/> </xsl:call-template> </select> </xsl:if> </td> <xsl:if test="$tasksAndtools_IsRTL = true()"> <td style="padding-right:5px; padding-left: 14px;white-space:nowrap ;"> <a id="TasksAndToolsGo" accesskey="G" title="Go" href="javascript:TATWP_jumpMenu()"> <img title="Go" alt="Go" border="0" src="/_layouts/images/icongo01RTL.gif" style="border-width:0px;" onmouseover="this.src='/_layouts/images/icongo02RTL.gif'" onmouseout="this.src='/_layouts/images/icongo01RTL.gif'"/> </a> </td> </xsl:if> <xsl:if test="$tasksAndtools_IsRTL = false()"> <td style="padding-right:14px; padding-left: 5px;white-space:nowrap ;"> <a id="TasksAndToolsGo" accesskey="G" title="Go" href="javascript:TATWP_jumpMenu()"> <img title="Go" alt="Go" border="0" src="/_layouts/images/icongo01.gif" style="border-width:0px;" onmouseover="this.src='/_layouts/images/icongo02.gif'" onmouseout="this.src='/_layouts/images/icongo01.gif'" /> </a> </td> </xsl:if> </tr> </table> </xsl:template> <xsl:template name="MainTemplate.body" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt"> <xsl:param name="Rows"/> <xsl:for-each select="$Rows"> <xsl:variable name="GroupStyle" select="'auto'"/> <option style="display:{$GroupStyle}" value="{ddwrt:EnsureAllowedProtocol(substring-before(@URL, ', '))}" > <xsl:value-of select="@Title"/> </option> </xsl:for-each> </xsl:template> </xsl:stylesheet></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.
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: }
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
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
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:
- Activate any features that are needed by the site
- Set the master page gallery settings
- Enable content type management
- Set your publishing options
- Fix the Content Type field so that it's a Choice field type by copying the source master page gallery
- Copy missing content types from the source site collection.
- Fix the Page Layouts and Site Templates
- 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)
- Copy any missing page layouts from the source
- Set all appropriate properties on each page layout
- Fix all the publishing pages PageLayout property to have the correct URL
- 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:
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.stsadm –o gl-convertsubsitetositecollection –sourceurl "http://intranet/testweb/" -targeturl "http://intranet/testsite/" -createmanagedpath -nofilecompression -owneremail "someone@example.com" -ownerlogin "domain\user" -deletesource
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.
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:
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 webapplication
stsadm -o gl-fixpublishingpagespagelayouturl -url "http://intranet" -scope site -pagelayout "http://intranet/_catalogs/masterpage/WelcomeLinks.aspx, /_catalogs/masterpage/WelcomeLinks.aspx".
Enumerate Available Page Layouts
I created this only because I needed to debug some issues I've been having with Page Layouts - try to convert a sub-site to a site collection and you'll see what I mean
. I doubt this command will be very useful to anyone but seeing as I've got it coded and working there was no sense in pulling the code. The command itself, gl-enumavailablepagelayouts, is very simple - it just outputs the available page layouts by calling GetAvailablePageLayouts() from a PublishingWeb object.
I'm outputting this code in XML as I wanted to display more things than what made sense in a flat file list (I suppose someone could use the output of this command for something else). The core code is shown below:
1: string url = keyValues["url"];
2: XmlDocument xmlDoc = new XmlDocument();
3: xmlDoc.AppendChild(xmlDoc.CreateElement("PageLayouts"));
4: // I added formatting just to make the xml easier to read when looking at it via the console.
5: xmlDoc.DocumentElement.AppendChild(xmlDoc.CreateWhitespace("\r\n"));
6:
7: using (SPSite site = new SPSite(url))
8: using (SPWeb web = site.OpenWeb())
9: {
10: PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
11:
12: foreach (PageLayout layout in pubweb.GetAvailablePageLayouts())
13: {
14: xmlDoc.DocumentElement.AppendChild(xmlDoc.CreateWhitespace("\t"));
15: XmlElement layoutNode = xmlDoc.CreateElement("PageLayout");
16: layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
17:
18: XmlElement node = xmlDoc.CreateElement("Name");
19: node.InnerText = layout.Name;
20: layoutNode.AppendChild(node);
21: layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
22:
23: node = xmlDoc.CreateElement("Title");
24: node.InnerText = layout.Title;
25: layoutNode.AppendChild(node);
26: layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
27:
28: node = xmlDoc.CreateElement("Id");
29: node.InnerText = layout.ListItem.ID.ToString();
30: layoutNode.AppendChild(node);
31: layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
32:
33: node = xmlDoc.CreateElement("AssociatedContentType");
34: if (layout.AssociatedContentType != null)
35: node.InnerText = layout.AssociatedContentType.Name;
36: layoutNode.AppendChild(node);
37: layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
38:
39: node = xmlDoc.CreateElement("ContentType");
40: if (layout.ListItem[FieldId.ContentType] != null)
41: node.InnerText = layout.ListItem[FieldId.ContentType].ToString();
42: layoutNode.AppendChild(node);
43: layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
44:
45: node = xmlDoc.CreateElement("Hidden");
46: if (layout.ListItem[FieldId.Hidden] != null)
47: node.InnerText = layout.ListItem[FieldId.Hidden].ToString();
48: else
49: node.InnerText = "false";
50: layoutNode.AppendChild(node);
51: layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t\t"));
52:
53: node = xmlDoc.CreateElement("FileUrl");
54: if (layout.ListItem.File != null)
55: node.InnerText = layout.ListItem.File.Url;
56: layoutNode.AppendChild(node);
57: layoutNode.AppendChild(xmlDoc.CreateWhitespace("\r\n\t"));
58:
59: xmlDoc.DocumentElement.AppendChild(layoutNode);
60: xmlDoc.DocumentElement.AppendChild(xmlDoc.CreateWhitespace("\r\n"));
61: }
62: }
63: output += xmlDoc.OuterXml;
The syntax of the command can be seen below:
C:\>stsadm -help gl-enumavailablepagelayouts
stsadm -o gl-enumavailablepagelayouts
Returns the list of page layouts available for the given site collection.
Parameters:
-url <site collection url>
Here’s an example of how to return the avilable page layouts for a publishing site site collection:
stsadm –o gl-enumavailablepagelayouts –url "http://intranet/"
The results of running the above command can be seen below:
1: <PageLayouts>
2: <PageLayout>
3: <Name>PageFromDocLayout.aspx</Name>
4: <Title>Article page with body only</Title>
5: <Id>24</Id>
6: <AssociatedContentType>Article Page</AssociatedContentType>
7: <ContentType>Page Layout</ContentType>
8: <Hidden>false</Hidden>
9: <FileUrl>_catalogs/masterpage/PageFromDocLayout.aspx</FileUrl>
10: </PageLayout>
11: <PageLayout>
12: <Name>ArticleLeft.aspx</Name>
13: <Title>Article page with image on left</Title>
14: <Id>21</Id>
15: <AssociatedContentType>Article Page</AssociatedContentType>
16: <ContentType>Page Layout</ContentType>
17: <Hidden>false</Hidden>
18: <FileUrl>_catalogs/masterpage/ArticleLeft.aspx</FileUrl>
19: </PageLayout>
20: <PageLayout>
21: <Name>ArticleRight.aspx</Name>
22: <Title>Article page with image on right</Title>
23: <Id>23</Id>
24: <AssociatedContentType>Article Page</AssociatedContentType>
25: <ContentType>Page Layout</ContentType>
26: <Hidden>false</Hidden>
27: <FileUrl>_catalogs/masterpage/ArticleRight.aspx</FileUrl>
28: </PageLayout>
29: <PageLayout>
30: <Name>ArticleLinks.aspx</Name>
31: <Title>Article page with summary links</Title>
32: <Id>22</Id>
33: <AssociatedContentType>Article Page</AssociatedContentType>
34: <ContentType>Page Layout</ContentType>
35: <Hidden>false</Hidden>
36: <FileUrl>_catalogs/masterpage/ArticleLinks.aspx</FileUrl>
37: </PageLayout>
38: <PageLayout>
39: <Name>RedirectPageLayout.aspx</Name>
40: <Title>Redirect Page</Title>
41: <Id>27</Id>
42: <AssociatedContentType>Redirect Page</AssociatedContentType>
43: <ContentType>Page Layout</ContentType>
44: <Hidden>false</Hidden>
45: <FileUrl>_catalogs/masterpage/RedirectPageLayout.aspx</FileUrl>
46: </PageLayout>
47: <PageLayout>
48: <Name>AdvancedSearchLayout.aspx</Name>
49: <Title>Advanced Search</Title>
50: <Id>87</Id>
51: <AssociatedContentType>Welcome Page</AssociatedContentType>
52: <ContentType>Page Layout</ContentType>
53: <Hidden>false</Hidden>
54: <FileUrl>_catalogs/masterpage/AdvancedSearchLayout.aspx</FileUrl>
55: </PageLayout>
56: <PageLayout>
57: <Name>BlankWebPartPage.aspx</Name>
58: <Title>Blank Web Part Page</Title>
59: <Id>28</Id>
60: <AssociatedContentType>Welcome Page</AssociatedContentType>
61: <ContentType>Page Layout</ContentType>
62: <Hidden>false</Hidden>
63: <FileUrl>_catalogs/masterpage/BlankWebPartPage.aspx</FileUrl>
64: </PageLayout>
65: <PageLayout>
66: <Name>PeopleSearchResults.aspx</Name>
67: <Title>People Search Results Page</Title>
68: <Id>90</Id>
69: <AssociatedContentType>Welcome Page</AssociatedContentType>
70: <ContentType>Page Layout</ContentType>
71: <Hidden>false</Hidden>
72: <FileUrl>_catalogs/masterpage/PeopleSearchResults.aspx</FileUrl>
73:
74: </PageLayout>
75: <PageLayout>
76: <Name>SearchMain.aspx</Name>
77: <Title>Search Page</Title>
78: <Id>88</Id>
79: <AssociatedContentType>Welcome Page</AssociatedContentType>
80: <ContentType>Page Layout</ContentType>
81: <Hidden>false</Hidden>
82: <FileUrl>_catalogs/masterpage/SearchMain.aspx</FileUrl>
83: </PageLayout>
84: <PageLayout>
85: <Name>SearchResults.aspx</Name>
86: <Title>Search Results Page</Title>
87: <Id>89</Id>
88: <AssociatedContentType>Welcome Page</AssociatedContentType>
89: <ContentType>Page Layout</ContentType>
90: <Hidden>false</Hidden>
91: <FileUrl>_catalogs/masterpage/SearchResults.aspx</FileUrl>
92: </PageLayout>
93: <PageLayout>
94: <Name>TabViewPageLayout.aspx</Name>
95: <Title>Site Directory Home</Title>
96: <Id>85</Id>
97: <AssociatedContentType>Welcome Page</AssociatedContentType>
98: <ContentType>Page Layout</ContentType>
99: <Hidden>false</Hidden>
100: <FileUrl>_catalogs/masterpage/TabViewPageLayout.aspx</FileUrl>
101: </PageLayout>
102: <PageLayout>
103: <Name>WelcomeLinks.aspx</Name>
104: <Title>Welcome page with summary links</Title>
105: <Id>3</Id>
106: <AssociatedContentType>Welcome Page</AssociatedContentType>
107: <ContentType>Page Layout</ContentType>
108: <Hidden>false</Hidden>
109: <FileUrl>_catalogs/masterpage/WelcomeLinks.aspx</FileUrl>
110: </PageLayout>
111: <PageLayout>
112: <Name>WelcomeTOC.aspx</Name>
113: <Title>Welcome page with table of contents</Title>
114: <Id>26</Id>
115: <AssociatedContentType>Welcome Page</AssociatedContentType>
116: <ContentType>Page Layout</ContentType>
117: <Hidden>false</Hidden>
118: <FileUrl>_catalogs/masterpage/WelcomeTOC.aspx</FileUrl>
119: </PageLayout>
120: <PageLayout>
121: <Name>DefaultLayout.aspx</Name>
122: <Title>Welcome page with Web Part zones</Title>
123: <Id>83</Id>
124: <AssociatedContentType>Welcome Page</AssociatedContentType>
125: <ContentType>Page Layout</ContentType>
126: <Hidden>false</Hidden>
127: <FileUrl>_catalogs/masterpage/DefaultLayout.aspx</FileUrl>
128: </PageLayout>
129: <PageLayout>
130: <Name>WelcomeSplash.aspx</Name>
131: <Title>Welcome splash page</Title>
132: <Id>25</Id>
133: <AssociatedContentType>Welcome Page</AssociatedContentType>
134: <ContentType>Page Layout</ContentType>
135: <Hidden>false</Hidden>
136: <FileUrl>_catalogs/masterpage/WelcomeSplash.aspx</FileUrl>
137: </PageLayout>
138: </PageLayouts>