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:

 1REM Create a test web for exporting
 2stsadm -o createweb -url "http://intranet/testweb" -sitetemplate "SPSTOPIC#0"
 3
 4REM Export the test web to the filesystem
 5stsadm -o export -url "http://intranet/testweb" -filename "c:\testweb" -includeusersecurity -versions 4 -nofilecompression -quiet
 6
 7REM Create a managed path for the new top level site
 8stsadm -o addpath -url "http://intranet/testsite" -type explicitinclusion
 9
10REM 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)
11stsadm -o createsite -url "http://intranet/testsite" -owneremail "someone@example.com" -ownerlogin "domain\username" -sitetemplate "SPSTOPIC#0"
12
13REM Import the site
14stsadm -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 (gl-fixpublishingpagespagelayouturl) 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).

gl-repairsitecollectionimportedfromsubsite

Here’s the code for the command:

  1using System;
  2using System.Collections;
  3using System.Collections.Generic;
  4using System.Collections.ObjectModel;
  5using System.Collections.Specialized;
  6using System.Data;
  7using System.Data.SqlClient;
  8using System.Diagnostics;
  9using System.Text;
 10using System.Text.RegularExpressions;
 11using System.Threading;
 12using System.Web.UI.WebControls.WebParts;
 13using System.Xml;
 14using Lapointe.SharePoint.STSADM.Commands.Lists;
 15using Lapointe.SharePoint.STSADM.Commands.SPValidators;
 16using Lapointe.SharePoint.STSADM.Commands.WebParts;
 17using Microsoft.SharePoint;
 18using Microsoft.SharePoint.Administration;
 19using Microsoft.SharePoint.Deployment;
 20using Microsoft.SharePoint.Navigation;
 21#if MOSS
 22using Microsoft.SharePoint.Publishing;
 23using Microsoft.SharePoint.Publishing.Fields;
 24using Microsoft.SharePoint.Publishing.Navigation;
 25using Microsoft.SharePoint.Publishing.WebControls;
 26#endif
 27using Microsoft.SharePoint.Utilities;
 28using Microsoft.SharePoint.WebPartPages;
 29using WebPart=Microsoft.SharePoint.WebPartPages.WebPart;
 30using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
 31using Lapointe.SharePoint.STSADM.Commands.Features;
 32
 33namespace Lapointe.SharePoint.STSADM.Commands.SiteCollectionSettings
 34{
 35    /// <summary>
 36    /// Repairs a site collection that has been imported from an exported sub-site.  
 37    /// Note that the sourceurl can be the actual source site or any site collection 
 38    /// that can be used as a model for the target.
 39    /// </summary>
 40    public class RepairSiteCollectionImportedFromSubSite : SPOperation
 41    {
 42        public RepairSiteCollectionImportedFromSubSite()
 43        {
 44            SPParamCollection parameters = new SPParamCollection();
 45            parameters.Add(new SPParam("sourceurl", "source", true, null, new SPUrlValidator(), "Please specify the source sub-site or model site collection."));
 46            parameters.Add(new SPParam("targeturl", "target", true, null, new SPUrlValidator(), "Please specify the target location of the new site collection to repair."));
 47            
 48            StringBuilder sb = new StringBuilder();
 49            sb.Append("\r\n\r\nRepairs 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.\r\n\r\nParameters:");
 50            sb.Append("\r\n\t-sourceurl <source location of the existing sub-site or model site collection>");
 51            sb.Append("\r\n\t-targeturl <target location for the new site collection>");
 52            
 53            Init(parameters, sb.ToString());
 54        }
 55
 56        #region ISPStsadmCommand Members
 57
 58        /// <summary>
 59        /// Gets the help message.
 60        /// </summary>
 61        /// <param name="command">The command.</param>
 62        /// <returns></returns>
 63        public override string GetHelpMessage(string command)
 64        {
 65            return HelpMessage;
 66        }
 67
 68        /// <summary>
 69        /// Runs the specified command.
 70        /// </summary>
 71        /// <param name="command">The command.</param>
 72        /// <param name="keyValues">The key values.</param>
 73        /// <param name="output">The output.</param>
 74        /// <returns></returns>
 75        public override int Execute(string command, StringDictionary keyValues, out string output)
 76        {
 77            output = string.Empty;
 78
 79            string sourceurl = Params["sourceurl"].Value.TrimEnd('/');
 80            string targeturl = Params["targeturl"].Value.TrimEnd('/');
 81
 82            RepairSite(sourceurl, targeturl, true);
 83
 84            return OUTPUT_SUCCESS;
 85        }
 86
 87        #endregion
 88
 89        /// <summary>
 90        /// Repairs the site.
 91        /// </summary>
 92        /// <param name="sourceurl">The sourceurl.</param>
 93        /// <param name="targeturl">The targeturl.</param>
 94        /// <param name="verbose">if set to <c>true</c> [verbose].</param>
 95        public static void RepairSite(string sourceurl, string targeturl, bool verbose)
 96        {
 97            Verbose = verbose;
 98            using (SPSite targetSite = new SPSite(targeturl.TrimEnd('/')))
 99            using (SPWeb targetWeb = targetSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(targeturl.TrimEnd('/'))])
100            using (SPSite sourceSite = new SPSite(sourceurl.TrimEnd('/')))
101            using (SPWeb sourceWeb = sourceSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(sourceurl.TrimEnd('/'))])
102            {
103                AddMissingFeatures(sourceSite, sourceWeb, targetSite, targetWeb);
104
105                try
106                {
107                    Log("Progress: Begin copying content types...");
108                    ContentTypes.CopyContentTypes.Copy(sourceurl, targeturl, verbose);
109                }
110                finally
111                {
112                    Log("Progess: End copying content types.");
113                }
114            }
115
116            // We need to re-open all the objects as some values such as content types need to be refreshed.
117            using (SPSite targetSite = new SPSite(targeturl.TrimEnd('/')))
118            using (SPWeb targetWeb = targetSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(targeturl.TrimEnd('/'))])
119            using (SPSite sourceSite = new SPSite(sourceurl.TrimEnd('/')))
120            using (SPWeb sourceWeb = sourceSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(sourceurl.TrimEnd('/'))])
121            {
122                SetMasterPageGallerySettings(sourceSite, targetSite, targetWeb);
123
124#if MOSS
125                PublishingWeb targetPublishingWeb = PublishingWeb.GetPublishingWeb(targetWeb);
126                PublishingSite targetPublishingSite = new PublishingSite(targetSite);
127                PublishingWeb sourcePublishingWeb = PublishingWeb.GetPublishingWeb(sourceWeb);
128                PublishingSite sourcePublishingSite = new PublishingSite(sourceSite);
129
130                FixPageLayoutsAndSiteTemplates(sourcePublishingSite, sourcePublishingWeb, targetPublishingSite, targetPublishingWeb, targetSite, targetWeb);
131#endif
132            }
133
134            TimerJob.ExecAdmSvcJobs.Execute(false, true);
135
136            using (SPSite targetSite = new SPSite(targeturl.TrimEnd('/')))
137            using (SPWeb targetWeb = targetSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(targeturl.TrimEnd('/'))])
138            using (SPSite sourceSite = new SPSite(sourceurl.TrimEnd('/')))
139            using (SPWeb sourceWeb = sourceSite.AllWebs[Utilities.GetServerRelUrlFromFullUrl(sourceurl.TrimEnd('/'))])
140            {
141#if MOSS
142                PublishingWeb targetPublishingWeb = PublishingWeb.GetPublishingWeb(targetWeb);
143                
144                FixPublishingPages(targetSite);
145
146                SetGlobalNavigation(targetPublishingWeb);
147
148                RetargetGroupedListingsWebPart.RetargetGroupedListings(targetWeb, "grouped listings");
149#endif
150                //RepairDiscussionLists(targetSite);
151
152                RetargetMiscWebParts(sourceSite, sourceWeb, targetWeb);
153            }
154        }
155
156        #region Worker Methods
157
158        #region Retargart Miscellaneous Web Parts
159
160        /// <summary>
161        /// Retargets the misc web parts.
162        /// </summary>
163        /// <param name="sourceSite">The source site.</param>
164        /// <param name="sourceWeb">The source web.</param>
165        /// <param name="targetWeb">The target web.</param>
166        internal static void RetargetMiscWebParts(SPSite sourceSite, SPWeb sourceWeb, SPWeb targetWeb)
167        {
168            StringDictionary listMap = new StringDictionary();
169
170            // Handle the primary web first then address the child webs
171            FindMatchingLists(listMap, sourceWeb, targetWeb);
172
173            string parentTargetUrl = targetWeb.ServerRelativeUrl;
174            string parentSourceUrl = sourceWeb.ServerRelativeUrl;
175
176            RetargetMiscWebPartsRecursiveHelper(listMap, parentSourceUrl, parentTargetUrl, sourceSite, targetWeb);
177
178            RetargetMiscWebParts(targetWeb, listMap);
179        }
180
181        /// <summary>
182        /// Recursive helper for retargetting the misc web parts.
183        /// </summary>
184        /// <param name="listMap">The list map.</param>
185        /// <param name="parentSourceUrl">The parent source URL.</param>
186        /// <param name="parentTargetUrl">The parent target URL.</param>
187        /// <param name="sourceSite">The source site.</param>
188        /// <param name="targetWeb">The target web.</param>
189        private static void RetargetMiscWebPartsRecursiveHelper(StringDictionary listMap, string parentSourceUrl, string parentTargetUrl, SPSite sourceSite, SPWeb targetWeb)
190        {
191            foreach (SPWeb childTargetWeb in targetWeb.Webs)
192            {
193                try
194                {
195                    string targetParentRelativeUrl = childTargetWeb.ServerRelativeUrl.Substring(parentTargetUrl.Length).TrimEnd('/');
196                    string sourceUrl = Utilities.ConcatServerRelativeUrls(parentSourceUrl, targetParentRelativeUrl).TrimEnd('/');
197
198
199                    using (SPWeb childSourceWeb = sourceSite.AllWebs[sourceUrl])
200                    {
201                        if (childSourceWeb.Exists)
202                        {
203                            FindMatchingLists(listMap, childSourceWeb, childTargetWeb);
204                        }
205                    }
206                    RetargetMiscWebPartsRecursiveHelper(listMap, parentSourceUrl, parentTargetUrl, sourceSite, childTargetWeb);
207
208                }
209                finally
210                {
211                    childTargetWeb.Dispose();
212                }
213            }
214        }
215
216        /// <summary>
217        /// Finds the matching lists.
218        /// </summary>
219        /// <param name="listMap">The list map.</param>
220        /// <param name="sourceWeb">The source web.</param>
221        /// <param name="targetWeb">The target web.</param>
222        private static void FindMatchingLists(StringDictionary listMap, SPWeb sourceWeb, SPWeb targetWeb)
223        {
224            foreach (SPList targetList in targetWeb.Lists)
225            {
226                try
227                {
228                    // Try to find a matching list in the source web
229                    SPList sourceList = sourceWeb.Lists[targetList.Title];
230                    listMap.Add(sourceList.ID.ToString(), targetList.ID.ToString());
231                }
232                catch (ArgumentException)
233                {
234                    // There is no matching source list - this should happen if our source is the actual source and not a model.
235                    // If the source is just a model then we're wasting our time here but unfortunately there's no way to
236                    // determine that without asking the user.
237                }
238            }
239        }
240
241        /// <summary>
242        /// Retargets the misc web parts for the target web.  Loops through all sub-webs and all files.
243        /// </summary>
244        /// <param name="targetWeb">The target web.</param>
245        /// <param name="listMap">The list map.</param>
246        private static void RetargetMiscWebParts(SPWeb targetWeb, StringDictionary listMap)
247        {
248            foreach (SPWeb subweb in targetWeb.Webs)
249            {
250                try
251                {
252                    RetargetMiscWebParts(subweb, listMap);
253                }
254                finally
255                {
256                    subweb.Dispose();
257                }
258            }
259
260            foreach (SPFile file in targetWeb.Files)
261            {
262                RetargetMiscWebParts(targetWeb, listMap, file);
263            }
264
265            foreach (SPList list in targetWeb.Lists)
266            {
267                foreach (SPListItem item in list.Items)
268                {
269                    if (item.File != null && item.File.Url.ToLowerInvariant().EndsWith(".aspx"))
270                    {
271                        RetargetMiscWebParts(targetWeb, listMap, item.File);
272                    }
273                }
274            }
275            targetWeb.Dispose();
276        }
277        
278        /// <summary>
279        /// Retargets the misc web parts on a specific file.  Loops through all web parts on the file.  Currently only
280        /// <see cref="DataFormWebPart"/> and <see cref="ContentByQueryWebPart"/> are considered.
281        /// </summary>
282        /// <param name="targetWeb">The target web.</param>
283        /// <param name="listMap">The list map.</param>
284        /// <param name="file">The file.</param>
285        private static void RetargetMiscWebParts(SPWeb targetWeb, StringDictionary listMap, SPFile file)
286        {
287            if (file == null)
288            {
289                return; // This should never be the case.
290            }
291            if (!Utilities.EnsureAspx(file.Url, true, false))
292                return; // We can only handle aspx and master pages.
293
294            if (file.InDocumentLibrary && Utilities.IsCheckedOut(file.Item) && !Utilities.IsCheckedOutByCurrentUser(file.Item))
295            {
296                return; // The item is checked out by a different user so leave it alone.
297            }
298
299            bool fileModified = false;
300            bool wasCheckedOut = true;
301
302            SPLimitedWebPartManager manager = null;
303            try
304            {
305                manager = targetWeb.GetLimitedWebPartManager(file.Url, PersonalizationScope.Shared);
306                SPLimitedWebPartCollection webParts = manager.WebParts;
307                
308                for (int i = 0; i < webParts.Count; i++)
309                {
310                    WebPart webPart = null;
311                    try
312                    {
313                        webPart = webParts[i] as WebPart;
314                        if (webPart == null)
315                        {
316                            webParts[i].Dispose();
317                            continue;
318                        }
319
320#if MOSS
321                        if (!(webPart is ContentByQueryWebPart || webPart is DataFormWebPart))
322                        {
323                            webPart.Dispose();
324                            continue;
325                        }
326#else
327                        if (!(webPart is DataFormWebPart))
328                        {
329                            webPart.Dispose();
330                            continue;
331                        }
332#endif
333                        foreach (string sourceId in listMap.Keys)
334                        {
335                            bool modified = false;
336
337                            ReplaceWebPartContent.Settings settings = new ReplaceWebPartContent.Settings();
338                            settings.LogFile = null;
339                            settings.Quiet = true;
340                            settings.Test = false;
341                            settings.SearchString = string.Format("(?i:{0})", sourceId);
342                            settings.ReplaceString = listMap[sourceId];
343                            settings.Publish = true;
344                            settings.UnsafeXml = true;
345
346                            Regex regex = new Regex(settings.SearchString);
347
348                            // As every web part has different requirements we are only going to consider a small subset.
349                            // Custom web parts will not be addressed as there's no interface that can utilized.
350#if MOSS
351                            if (webPart is ContentByQueryWebPart)
352                            {
353                                ContentByQueryWebPart wp = (ContentByQueryWebPart)webPart;
354                                webPart = ReplaceWebPartContent.ReplaceValues(
355                                    targetWeb,
356                                    file,
357                                    settings,
358                                    wp,
359                                    regex,
360                                    ref manager,
361                                    ref wasCheckedOut,
362                                    ref modified);
363                            }
364                            else 
365#endif
366                            if (webPart is DataFormWebPart)
367                            {
368                                DataFormWebPart wp = (DataFormWebPart)webPart;
369                                webPart = ReplaceWebPartContent.ReplaceValues(
370                                    targetWeb,
371                                    file,
372                                    settings,
373                                    wp,
374                                    regex,
375                                    ref manager,
376                                    ref wasCheckedOut,
377                                    ref modified);
378                            }
379
380                            if (modified)
381                                manager.SaveChanges(webPart);
382
383                            if (modified)
384                                fileModified = true;
385                        }
386                    }
387                    finally
388                    {
389                        if (webPart != null)
390                            webPart.Dispose();
391                    }
392                }
393                if (fileModified)
394                    file.CheckIn(
395                        "Checking in changes to list item due to retargetting of web part as a result of converting a sub-site to a site collection.");
396
397                if (file.InDocumentLibrary && fileModified && !wasCheckedOut)
398                    PublishItems.PublishListItem(file.Item, file.Item.ParentList,
399                                                 new PublishItems.Settings(true, false, null),
400                                                 "\"stsadm.exe -o repairsitecollectionimportedfromsubsite | convertsubsitetositecollection\"");
401            }
402            finally
403            {
404                if (manager != null)
405                {
406                    manager.Web.Dispose(); // manager.Dispose() does not dispose of the SPWeb object and results in a memory leak.
407                    manager.Dispose();
408                }
409            }
410        }
411
412        #endregion
413
414#if MOSS
415
416        #region Set Global Navigation
417
418        /// <summary>
419        /// Sets the global navigation.
420        /// </summary>
421        /// <param name="targetPublishingWeb">The target publishing web.</param>
422        private static void SetGlobalNavigation(PublishingWeb targetPublishingWeb)
423        {
424            SPNavigationNodeCollection globalNodes = targetPublishingWeb.GlobalNavigationNodes;
425            SPNavigationNodeCollection currentNodes = targetPublishingWeb.CurrentNavigationNodes;
426
427            if (globalNodes.Count > 0)
428            {
429                return;
430            }
431
432            SetGlobalNavigationRecursiveHelper(currentNodes, globalNodes);
433        }
434
435        /// <summary>
436        /// Recursive routine for setting the global navigation (necessary to handle child elements).
437        /// </summary>
438        /// <param name="currentNodes">The current nodes.</param>
439        /// <param name="globalNodes">The global nodes.</param>
440        private static void SetGlobalNavigationRecursiveHelper(SPNavigationNodeCollection currentNodes, SPNavigationNodeCollection globalNodes)
441        {
442            // We're going to copy the current nodes in order to set the global nodes (so use current as the default)
443            for (int i = 0; i < currentNodes.Count; i++)
444            {
445                SPNavigationNode currentNode = currentNodes[i];
446                NodeTypes type = NodeTypes.None;
447                if (currentNode.Properties["NodeType"] != null)
448                    type = (NodeTypes)Enum.Parse(typeof(NodeTypes), (string)currentNode.Properties["NodeType"]);
449
450                SPNavigationNode node = SPNavigationSiteMapNode.CreateSPNavigationNode(
451                    currentNode.Title, currentNode.Url, type, globalNodes);
452                foreach (DictionaryEntry de in currentNode.Properties)
453                {
454                    node.Properties[de.Key] = de.Value;
455                }
456                node.Update();
457                node.MoveToLast(globalNodes);
458
459                if (currentNode.Children.Count > 0)
460                    SetGlobalNavigationRecursiveHelper(currentNode.Children, node.Children);
461            }
462        }
463
464        #endregion
465
466
467        /// <summary>
468        /// Fixes the publishing pages.
469        /// </summary>
470        /// <param name="targetSite">The target site.</param>
471        private static void FixPublishingPages(SPSite targetSite)
472        {
473            Log("Progress: Begin fixing publishing pages...");
474            try
475            {
476                foreach (SPWeb web in targetSite.AllWebs)
477                {
478                    try
479                    {
480                        if (!PublishingWeb.IsPublishingWeb(web))
481                            continue;
482
483                        PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
484
485                        Pages.FixPublishingPagesPageLayoutUrl.FixPages(pubweb);
486                    }
487                    finally
488                    {
489                        web.Dispose();
490                    }
491                }
492            }
493            finally
494            {
495                Log("Progress: End fixing publishing pages.");
496            }
497        }
498
499        /// <summary>
500        /// Fixes the page layouts and sets the site templates.
501        /// </summary>
502        /// <param name="sourcePublishingSite">The source publishing site.</param>
503        /// <param name="sourcePublishingWeb">The source publishing web.</param>
504        /// <param name="targetPublishingSite">The target publishing site.</param>
505        /// <param name="targetPublishingWeb">The target publishing web.</param>
506        /// <param name="targetSite">The target site.</param>
507        /// <param name="targetWeb">The target web.</param>
508        private static void FixPageLayoutsAndSiteTemplates(PublishingSite sourcePublishingSite, PublishingWeb sourcePublishingWeb, PublishingSite targetPublishingSite, PublishingWeb targetPublishingWeb, SPSite targetSite, SPWeb targetWeb)
509        {
510            try
511            {
512                Log("Progress: Begin fixing page layouts and site templates...");
513                // Next thing we need to do is reset the "__PageLayouts" property - after doing the import
514                // this value (targetWeb.AllProperties["__PageLayouts"]) will equal "__inherit" but it should
515                // be either an empty string or an xml list of layouts.  Fixing this resolves the xml error
516                // that we receive when going to "targetSite/_layouts/AreaTemplateSettings.aspx":
517                // "Data at the root level is invalid. Line 1, position 1".  This error occurs when GetAvailablePageLayouts()
518                // is called - within that method there's call to get to GetEffectiveAvailablePageLayoutsAsString()
519                // which in this case returns back "__inherit" but should return back "" - as a result the next
520                // statement after that is a call to IsPropertyAllowingAll - this returns back false because it's not
521                // an empty string - as a result an XmlDocument.LoadXml() call is made which fails because "__inherit"
522                // is not valid xml.
523
524                if (!string.IsNullOrEmpty(targetWeb.AllProperties["__PageLayouts"] as string))
525                {
526                    Log(
527                        "Progress: Setting \"__PageLayouts\" web property to string.Empty (current value is \"{0}\")...",
528                        targetWeb.AllProperties["__PageLayouts"] as string);
529                    targetWeb.AllProperties["__PageLayouts"] = string.Empty;
530                    // We need to update the web so that the changes above stick and the following code can execute.
531                    targetWeb.Update();
532                }
533
534                // Reset to make sure everything gets propagated correctly. (Note - this may need to be moved to after we copy the page layouts)
535                Log("Progress: Setting available page layouts...");
536                if (sourcePublishingWeb.IsAllowingAllPageLayouts)
537                    targetPublishingWeb.AllowAllPageLayouts(true);
538                else
539                    targetPublishingWeb.SetAvailablePageLayouts(sourcePublishingWeb.GetAvailablePageLayouts(), true);
540
541                // Reset the site templates settings.
542                if (sourcePublishingWeb.IsAllowingAllWebTemplates)
543                {
544                    Log("Progress: Allowing all site templates...");
545                    targetPublishingWeb.AllowAllWebTemplates(true);
546                }
547                else
548                {
549                    Log("Progress: Setting explicit site template settings...");
550                    // Handle site templates set explicitly.
551                    Collection<SPWebTemplate> list = new Collection<SPWebTemplate>();
552                    foreach (SPWebTemplate template in sourcePublishingWeb.GetAvailableCrossLanguageWebTemplates())
553                    {
554                        list.Add(template);
555                    }
556                    targetPublishingWeb.SetAvailableCrossLanguageWebTemplates(list, true);
557
558                    foreach (SPLanguage lang in sourcePublishingWeb.Web.RegionalSettings.InstalledLanguages)
559                    {
560                        list = new Collection<SPWebTemplate>();
561                        foreach (
562                            SPWebTemplate template in sourcePublishingWeb.GetAvailableWebTemplates((uint) lang.LCID))
563                        {
564                            list.Add(template);
565                        }
566                        if (list.Count > 0)
567                            targetPublishingWeb.SetAvailableWebTemplates(list, (uint) lang.LCID, true);
568                    }
569                }
570
571                Log("Progress: Setting page layouts...");
572                foreach (PageLayout layout in sourcePublishingSite.GetPageLayouts(false))
573                {
574                    // We need to reset all the page layouts to match that of the source site
575                    // (after the import all the layouts are messed up and treated as plain files and not page layouts
576                    // because the content type is no longer associated).
577                    PageLayoutCollection targetSiteLayouts = targetPublishingSite.GetPageLayouts(false);
578                    PageLayout tempPageLayout = null;
579
580                    try
581                    {
582                        tempPageLayout = targetSiteLayouts[
583                            targetPublishingSite.PageLayouts.LayoutsDocumentLibrary.RootFolder.ServerRelativeUrl.
584                                TrimStart('/') + "/" + layout.Name];
585                    }
586                    catch (ArgumentException)
587                    {
588                    }
589
590                    if (tempPageLayout == null)
591                    {
592                        // We didn't find an item in the collection so let's attempt to add it back.
593                        Log("Progress: Source layout {0} not found in target layout collection, searching files...",
594                            layout.Name);
595                        // First we need to see if the file is already there.
596                        SPFile file = null;
597                        foreach (SPListItem item in targetPublishingSite.PageLayouts.LayoutsDocumentLibrary.Items)
598                        {
599                            if (item.Name == layout.Name)
600                            {
601                                // We found the file so exit out of the loop.
602                                file = item.File;
603                                Log("Progress: Layout file found.");
604                                break;
605                            }
606                        }
607
608                        if (file == null)
609                        {
610                            Log("Progress: Layout file Not found.");
611                            SPList targetMasterGallery =
612                                targetSite.RootWeb.GetCatalog(SPListTemplateType.MasterPageCatalog);
613                            SPFolder targetMasterGalleryFolder = targetMasterGallery.RootFolder;
614
615                            try
616                            {
617                                Log("Progress: Copying page layout {0} from source site...", layout.Name);
618                                // We couldn't find a file so copy the file from the source.
619                                file = targetMasterGalleryFolder.Files.Add(layout.Name,
620                                                                           layout.ListItem.File.OpenBinary());
621                            }
622                            catch (Exception ex)
623                            {
624                                Log(
625                                    string.Format("ERROR: Unable to copy page layout from source location - {0}",
626                                                  ex.Message), EventLogEntryType.Error);
627                                continue;
628                            }
629                        }
630                        if (file == null)
631                        {
632                            Log("WARNING: Unable to copy page layout from source location.", EventLogEntryType.Warning);
633                            continue;
634                        }
635
636                        // Reset all the properties on the file so that it's flagged as a PageLayout content type.
637                        Log("Progress: Setting page layout properties...");
638                        SPListItem listItem = file.Item;
639                        listItem[FieldId.Hidden] = layout.ListItem[FieldId.Hidden];
640                        listItem[FieldId.Title] = layout.ListItem[FieldId.Title];
641                        listItem[FieldId.ContentType] =
642                            GetContentType(targetSite.RootWeb, ContentTypeId.PageLayout).Name;
643                        listItem[FieldId.AssociatedContentType] =
644                            new ContentTypeIdFieldValue(GetContentType(targetSite.RootWeb,
645                                                                       layout.AssociatedContentType.Id));
646                        listItem[FieldId.AssociatedVariations] = layout.ListItem[FieldId.AssociatedVariations];
647                        listItem[SPBuiltInFieldId.File_x0020_Type] = layout.ListItem[SPBuiltInFieldId.File_x0020_Type];
648
649                        // Add any additional properties specific to the page layout
650                        PageLayout newLayout = new PageLayout(listItem);
651                        newLayout.Description = layout.Description;
652                        newLayout.Title = layout.Title;
653                        if (!string.IsNullOrEmpty(layout.PreviewImageUrl))
654                        {
655                            string previewImageUrl =
656                                targetWeb.ServerRelativeUrl +
657                                layout.PreviewImageUrl.Substring(
658                                    layout.PreviewImageUrl.IndexOf("/_catalogs/"));
659                            newLayout.PreviewImageUrl = targetSite.MakeFullUrl(previewImageUrl);
660                        }
661                        listItem.SystemUpdate();
662
663                        if (Utilities.IsCheckedOut(file.Item))
664                        {
665                            Log("Progress: Checking in page layout file...");
666                            file.CheckIn("", SPCheckinType.MajorCheckIn);
667                            if (file.Item.ModerationInformation != null)
668                                file.Approve("");
669                        }
670                    }
671                }
672            }
673            finally
674            {
675                Log("Progress: End fixing page layouts and site templates.");
676            }
677        }
678#endif
679
680        /// <summary>
681        /// Adds the missing features.
682        /// </summary>
683        /// <param name="sourceSite">The source site.</param>
684        /// <param name="sourceWeb">The source web.</param>
685        /// <param name="targetSite">The target site.</param>
686        /// <param name="targetWeb">The target web.</param>
687        public static void AddMissingFeatures(SPSite sourceSite, SPWeb sourceWeb, SPSite targetSite, SPWeb targetWeb)
688        {
689            Log("Progress: Begin adding missing features...");
690            try
691            {
692                // Set any features that need to be enabled.
693                Dictionary<SPFeatureScope, SPFeatureCollection> activeFeatures =
694                    new Dictionary<SPFeatureScope, SPFeatureCollection>();
695                if (sourceSite != null)
696                {
697                    activeFeatures[SPFeatureScope.WebApplication] = sourceSite.WebApplication.Features;
698                    activeFeatures[SPFeatureScope.Site] = sourceSite.Features;
699                }
700                if (sourceWeb != null)
701                    activeFeatures[SPFeatureScope.Web] = sourceWeb.Features;
702
703                // Note that you should be able to use the ActivationDependencies property of the SPDefinition,
704                // however, I found that this property is not reliable (good example: Publishing is dependent
705                // on PublishingSite but when you view the ActivationDependencies for the Publishing definition
706                // it shows zero dependencies.
707                Queue<SPFeatureDefinition> queuedFeatures = new Queue<SPFeatureDefinition>();
708                // For some reason these need to be added before the rest...
709                if (SPFarm.Local.FeatureDefinitions["BaseSite"] != null)
710                    queuedFeatures.Enqueue(SPFarm.Local.FeatureDefinitions["BaseSite"]);
711                if (SPFarm.Local.FeatureDefinitions["PremiumSite"] != null)
712                    queuedFeatures.Enqueue(SPFarm.Local.FeatureDefinitions["PremiumSite"]);
713                if (SPFarm.Local.FeatureDefinitions["PublishingSite"] != null)
714                    queuedFeatures.Enqueue(SPFarm.Local.FeatureDefinitions["PublishingSite"]);
715
716                foreach (SPFeatureDefinition definition in SPFarm.Local.FeatureDefinitions)
717                {
718                    try
719                    {
720                        if (definition.Scope == SPFeatureScope.Farm)
721                            continue;
722
723                        if (!queuedFeatures.Contains(definition))
724                            queuedFeatures.Enqueue(definition);
725                    }
726                    catch (Exception ex)
727                    {
728                        Log("ERROR: {0}", ex.Message);
729                    }
730                }
731                while (queuedFeatures.Count > 0)
732                {
733                    SPFeatureDefinition definition = queuedFeatures.Dequeue();
734                    if (definition == null)
735                        continue;
736
737                    SPFeatureScope scope = SPFeatureScope.ScopeInvalid;
738                    try
739                    {
740                        scope = definition.Scope;
741                    }
742                    catch (Exception ex)
743                    {
744                        Log("ERROR: {0}", ex.Message);
745                        continue;
746                    }
747                    Guid featureID = definition.Id;
748                    if (activeFeatures[scope] != null)
749                    {
750                        bool isActive = (activeFeatures[scope][featureID] != null);
751                        if (!isActive)
752                            continue;
753
754                        try
755                        {
756                            switch (scope)
757                            {
758                                case SPFeatureScope.Site:
759                                    if (targetSite != null && targetSite.Features[featureID] == null)
760                                    {
761                                        Log("Progress: Activating site scoped feature \"{0}\"...", definition.DisplayName);
762                                        targetSite.Features.Add(featureID);
763                                    }
764                                    break;
765                                case SPFeatureScope.Web:
766                                    if (targetWeb != null && targetWeb.Features[featureID] == null)
767                                    {
768                                        Log("Progress: Activating web scoped feature \"{0}\"...", definition.DisplayName);
769                                        targetWeb.Features.Add(featureID);
770                                    }
771                                    break;
772                                case SPFeatureScope.WebApplication:
773                                    if (sourceSite != null && sourceSite.WebApplication.Features[featureID] == null)
774                                    {
775                                        Log("Progress: Activating web application scoped feature \"{0}\"...", definition.DisplayName);
776                                        sourceSite.WebApplication.Features.Add(featureID);
777                                    }
778                                    break;
779                                default:
780                                    continue;
781                            }
782                        }
783                        catch (ArgumentOutOfRangeException)
784                        {
785                            // We couldn't add the item most likely due a dependent content type that has not yet been added.
786                            queuedFeatures.Enqueue(definition);
787                        }
788                        catch (InvalidOperationException)
789                        {
790                            // We couldn't add the item most likely due to dependencies so add to the back of the queue.
791                            queuedFeatures.Enqueue(definition);
792                        }
793                        catch (ArgumentException)
794                        {
795                            // We couldn't add the item most likely due to dependencies so add to the back of the queue.
796                            queuedFeatures.Enqueue(definition);
797                        }
798                        catch (SPException ex)
799                        {
800                            // This can occur occasionally if the feature does some external modifications (should be rare)
801                            if (ex.Message == "The web being updated was changed by an external process.")
802                            {
803                                if (targetWeb != null)
804                                    targetWeb.Close();
805                                if (targetSite != null)
806                                    targetWeb = targetSite.OpenWeb();
807                                queuedFeatures.Enqueue(definition);
808                            }
809                            else
810                            {
811                                Log("WARNING: Unable to activate feature '{0} ({1})'\r\n{2}",
812                                                  definition.DisplayName, definition.Name, ex.Message);
813                            }
814                        }
815                    }
816                }
817            }
818            finally
819            {
820                Log("Progress: End adding missing features.");
821            }
822        }
823
824
825        /// <summary>
826        /// Sets the master page gallery settings.
827        /// </summary>
828        /// <param name="sourceSite">The source site.</param>
829        /// <param name="targetSite">The target site.</param>
830        /// <param name="targetWeb">The target web.</param>
831        internal static void SetMasterPageGallerySettings(SPSite sourceSite, SPSite targetSite, SPWeb targetWeb)
832        {
833            Log("Progress: Begin setting master page gallery settings...");
834            try
835            {
836                SPList targetMasterGallery = targetSite.RootWeb.GetCatalog(SPListTemplateType.MasterPageCatalog);
837                SPList sourceMasterGallery = sourceSite.RootWeb.GetCatalog(SPListTemplateType.MasterPageCatalog);
838
839                // Need to make sure the master gallery has it's content types enabled (get's disabled during the import) 
840                // and make sure all other settings are set to match the source.
841                Log("Progress: Setting versioning properties...");
842                targetMasterGallery.ContentTypesEnabled = true;
843                targetMasterGallery.EnableModeration = sourceMasterGallery.EnableModeration;
844                targetMasterGallery.EnableVersioning = sourceMasterGallery.EnableVersioning;
845                targetMasterGallery.EnableMinorVersions = sourceMasterGallery.EnableMinorVersions;
846                targetMasterGallery.MajorVersionLimit = sourceMasterGallery.MajorVersionLimit;
847                try
848                {
849                    targetMasterGallery.MajorWithMinorVersionsLimit = sourceMasterGallery.MajorWithMinorVersionsLimit;
850                }
851                catch (NotSupportedException)
852                {
853                    // If we're here then something is wrong with the source list so just ignore the error.
854                }
855                targetMasterGallery.DraftVersionVisibility = sourceMasterGallery.DraftVersionVisibility;
856                targetMasterGallery.ForceCheckout = sourceMasterGallery.ForceCheckout;
857
858                targetMasterGallery.Update();
859
860                SPField contentTypeField = targetMasterGallery.Fields.GetFieldByInternalName("ContentType");
861                if (contentTypeField.Type != SPFieldType.Choice)
862                {
863                    Log("Progress: ContentType field must be a Choice column (currently is {0})", contentTypeField.TypeAsString);
864                    if (contentTypeField.Type == SPFieldType.Text)
865                    {
866#if MOSS
867                        if (PublishingWeb.IsPublishingWeb(targetWeb))
868                        {
869                            Log("Progress: ContentType field is a Text type in a publishing web, attempting to activate PublishingResources to correct...");
870                            Guid publishingResourcesFeatureId = new Guid("AEBC918D-B20F-4a11-A1DB-9ED84D79C87E");
871                            FeatureHelper fh = new FeatureHelper();
872                            fh.ActivateDeactivateFeatureAtSite(targetSite, true, publishingResourcesFeatureId, true, false);
873
874                            // Get the list again to make sure we're not dealing with a cached copy
875                            targetMasterGallery = targetSite.RootWeb.GetCatalog(SPListTemplateType.MasterPageCatalog);
876                            contentTypeField = targetMasterGallery.Fields.GetFieldByInternalName("ContentType");
877                            if (contentTypeField.Type != SPFieldType.Choice)
878                            {
879                                Log("Progress: Failed to fix ContentType field via PublishingResources feature activation, attempting to copy entire gallery from source...");
880                                CopyList cp = new CopyList();
881                                cp.Copy(sourceSite.MakeFullUrl(sourceMasterGallery.RootFolder.ServerRelativeUrl), 
882                                    targetSite.Url, null, true, false, true, true, false, false, SPIncludeVersions.All, SPUpdateVersions.Overwrite, true, false, false, false, false);
883
884                            }
885                        }
886#endif
887
888                    }
889                }
890            }
891            finally
892            {
893                Log("Progress: End setting master page gallery settings.");
894            }
895        }
896
897        #endregion
898
899        #region Helper Methods
900
901        /// <summary>
902        /// Gets the type of the content.
903        /// </summary>
904        /// <param name="web">The web.</param>
905        /// <param name="contentTypeId">The content type id.</param>
906        /// <returns></returns>
907        private static SPContentType GetContentType(SPWeb web, SPContentTypeId contentTypeId)
908        {
909            SPContentType type = web.AvailableContentTypes[contentTypeId];
910            if (type == null)
911            {
912                throw new SPException("Content Type Not Found In Web " + contentTypeId + ", " + web.Url);
913            }
914            return type;
915        }
916
917        /// <summary>
918        /// Gets the folder by id.
919        /// </summary>
920        /// <param name="list">The list.</param>
921        /// <param name="id">The id.</param>
922        /// <returns></returns>
923        private static SPListItem GetFolderById(SPList list, int id)
924        {
925            foreach (SPListItem folder in list.Folders)
926            {
927                if (id == folder.ID)
928                    return folder;
929            }
930            return null;
931        }
932        #endregion
933    }
934}

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 NameAvailabilityBuild Date
gl-repairsitecollectionimportedfromsubsiteWSS 3, MOSS 2007Released: 9/4/2007, Updated: 7/6/2009
Parameter NameShort FormRequiredDescriptionExample Usage
sourceurlsourceYesThe URL to the source sub-site to convert.-sourceurl http://portal/subsite, -source http://portal/subsite
targeturltargetYesThe 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/"

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 NameAvailabilityBuild Date
gl-convertsubsitetositecollectionWSS 3, MOSS 2007Released: 9/4/2007, Updated: 7/6/2009
Parameter NameShort FormRequiredDescriptionExample Usage
sourceurlsourceYesThe URL to the source sub-site to convert.-sourceurl http://portal/subsite, -source http://portal/subsite
targeturltargetYesThe URL of the new site collection to create.-targeturl http://portal/sites/site, -target http://portal/sites/site
owneremailoeYesThe 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
createmanagedpathcreatepathNoCreate a new managed path for the site collection.-createmanagedpath, -createpath
haltonwarningwarningNoStop execution of the command if a warning event occurs during the export or import process.-haltonwarning, -warning
haltonfatalerrorerrorNoStop execution of the command if a fatal error occurs during the export or import process.-haltonfatalerror, -error
exportedfilefileNoUse a previously exported site (created using stsadm’s export command).-exportedfile c:\exportdata\site, -file c:\exportdata\site
nofilecompressionNoDo not compress the site when exporting (or if previously exported use an uncompressed file for the import).-nofilecompression
ownerloginolIf 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
ownernameonNoThe site owner’s display name.-ownername "Gary Lapointe", -on "Gary Lapointe"
secondaryemailseNoThe 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
secondaryloginslIf 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
secondarynamesnNoThe secondary site owner’s display name.-secondaryname "Pam Lapointe", -sn "Pam Lapointe"
lcidNoA valid locale ID, such as “1033” for English. You must specify this parameter when using a non-English template.-lcid 1033
titletNoThe 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"
descriptiondescNoDescription 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"
hostheaderwebapplicationurlhhurlNoA 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
quotaNoThe quota template to apply to sites created on the virtual server.-quota Portal
deletesourceNoDelete the source site after conversion (only recommended if significant testing has occurred).-deletesource
createsiteinnewdbnewdbNoCreate the site collection in a content database.-createsiteinnewdb, -newdb
createsiteindbdbNoCreate the site collection in an existing content database.-createsiteindb, -db
databaseserverdsNoThe database server containing the specified content database. If not specified then the default database server is used.-databaseserver spsql1, -ds spsql1
databaseuserduNoThe administrator user name for the SQL Server database.-databaseuser domain\user, -du domain\user
databasepassworddpNoThe password that corresponds to the administrator user name for the SQL Server database.-databasepassword password, -dp password
databasenamednYes 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
suppressaftereventssaeNoDisable the firing of After events when creating or modifying files or list items during the import.-suppressafterevents, -sae
verbosevNoDisplays 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.