I was working on a project last Fall where a client of mine had a single site collection for their entire document library which was expected to be over 1TB. As a result of the large size of the site collection we decided to break it up into multiple site collections each contained within their own content database (we ended up with 12 in the end). The problem was that when we migrated all the libraries to the new site collections we ended up with hundreds of broken links due to navigation items (as well as web parts and list items) pointing to the original document libraries. The client was prepared to manually go through all the links to correct them but this just seemed a bit crazy to me so I quickly threw together a new command which would recursively go through all the webs and fix any navigation links that were pointing to the old content (I already had something for the web parts and list items). I named this command gl-replacenavigationurls. I actually had this command completed and available since November some time but I completely forgot about it so it never got documented – oops . I wonder if there’s any other commands that I’ve created but didn’t document? Hmm…
The complete code is shown below (note that at present I’m only supporting MOSS for this one as I’ve not had time to do any WSS rework for it):
The help for the command is shown below:
C:\>stsadm -help gl-replacenavigationurls stsadm -o gl-replacenavigationurls Replaces URL values in the current and global navigation matching the provided search pattern. Parameters: -url <url to search> -searchstring <regular expression string to search for> -replacestring <replacement string> [-quiet] [-scope <WebApplication | Site | Web> (defaults to Site)]
The following table summarizes the command and its various parameters:
|Command Name||Availability||Build Date|
|gl-replacenavigationurls||MOSS 2007||Released: 1/18/2009
|Parameter Name||Short Form||Required||Description||Example Usage|
|url||Yes||URL of the web application or site collection.||-url "http://portal"|
|searchstring||search||Yes||The regular expression search string.||-searchstring "(?i:/doccenter/IT)"
|replacestring||replace||Yes||The replace string.||-replacestring "/docs/IT"
|quiet||q||No||Specify to suppress status information while the command is running.||-quiet
|scope||s||No – defaults to site||The scope to use. Valid values are “WebApplication”, “Site”, and “Web”.||-scope site
The following is an example of how to replace all references to “/doccenter/IT” with “/docs/IT”:
stsadm -o gl-replacenavigationurls –url "http://portal" –searchstring "(?i:/doccenter/IT)" –replacestring "/docs/IT" –scope WebApplication
A while back I had created a command which allowed you to set the navigation elements/nodes for a given site. The command allowed you to also set a couple of basic switches that appear on the Site Navigation Settings page (Home > Site Settings > Modify Navigation) but it didn't allow you to set all the flags. I thought about possibly modifying this command to allow all the flags to be set but then decided it would introduce some complexities that were just not worth introducing.
In the end I decided to create a new command, gl-setnavigationsettings, which would allow you to set all the values found on the above mentioned page with the exception of the navigation nodes themselves. Fortunately, the code for this was extremely simple (especially considering I already had chunks that I could use thanks to the other command I had created). The code simply sets properties on the PublishingWeb object based on parameters provided by the user:
The syntax of the command can be seen below:
C:\>stsadm -help gl-setnavigationsettings stsadm -o gl-setnavigationsettings Sets the navigation settings for a web site (use gl-setnavigationnodes to change the actual nodes that appear). Parameters: -url <site collection url> [-showsubsites <true | false>] [-showpages <true | false>] [-sortmethod <automatic | manualwithautomaticpagesorting | manual>] [-autosortmethod <title | createddate | lastmodifieddate>] [-sortascending <true | false>] [-inheritglobalnav <true | false>] [-currentnav <inheritparent | currentsiteandsiblings | currentsiteonly>]Here's an example of how to set various settings on a given web:
stsadm -o gl-setnavigationsettings -url "http://intranet/sitedirectory" -showsubsites true -showpages true -sortmethod automatic -autosortmethod title -sortascending true -inheritglobalnav false -currentnav currentsiteandsiblings
As part of my upgrade I've got several hundred web sites that need to be moved around (either from one web application to another or just to a different location with the same web app or site collection). I figured that I needed an easy way to do this - the usual way is to either use the browser and go to the content and structure page to move webs around within a site collection or use the stsadm import and export commands to move between site collections.
What I wanted was a single command that I could call to move a web around regardless of what my target was (the same site collection or a different one). To move a web within the same site collection is really easy - you just set the ServerRelativeUrl property of the SPWeb object to the new path and then call Update() on the web object:
Moving a web to a different site collection requires a bit more effort. This is where you end up having to export the source web, create a placeholder web at the target, and then import the exported site to the placeholder site. Fortunately I already had some code to handle calling out to the stsadm import and export commands so I didn't have re-invent that (it's much easier to use these built in commands than to try and recreate them via the content deployment API - see the gl-importlist, gl-exportlist, and gl-copylist commands that I created earlier for examples of what's involved). Thanks to those existing helper methods the code became fairly simple and quick to write:The command I created, gl-moveweb, is detailed below.
Using this command is pretty straightforward - you just pass in the url of the source web and the url of the new parent web. I've got a couple of checks to make sure that you're not trying to set the parent to itself. If you're moving the web to a new site collection then you have 3 optional parameters of which only the "-includeusersecurity" has any real affect (the others just stop either the import or export if there's a warning or fatal error depending on which option you provide). The syntax of the command can be seen below:
C:\>stsadm -help gl-moveweb stsadm -o gl-moveweb Moves a web. Parameters: -url <url of web to move> -parenturl <url of parent web> [-haltonwarning (only considered if moving to a new site collection)] [-haltonfatalerror (only considered if moving to a new site collection)] [-includeusersecurity (only considered if moving to a new site collection)] [-retainobjectidentity (only considered if moving to a new site collection)]
Here’s an example of how to move a web within the same site collection:
stsadm –o gl-moveweb -url "http://intranet/topics/divisions" -parenturl "http://intranet"
Here’s an example of how to move a web to a different site collection:
Update 9/21/2007: I've 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).
stsadm –o gl-moveweb -url "http://intranet/sites/projectATeamSite" -parenturl "http://teamsites/projects" -includeusersecurity -haltonfatalerror
Update 10/8/2007: I've enhanced the command to take advantage of another new command I created: gl-import2 (object IDs are now maintained during the move so items dependent on a specific ID should no longer break).
Update 10/12/2007: I've fixed some issues with the retainobjectidentity setting and the use of the import2 command. As a result I've decided to make the retainobjectidentity an option specified via a parameter rather than the default behavior. The main issue is due to the fact that when importing sites using the retainObjectIdentity option the exported site must be a child of the root site or else things get really messed up (as a result I move the site to be under the root site, then export, then import, and then rename the site to match the original plus a possible suffix if the original name already existed at the target). The other issue I found is that for some reason some web parts will be imported twice on a given page - I figure though that this is easier to fix (simply delete the extra web part) than the alternative of having to retarget all the web parts to their new source.
Update 10/17/2007: I've made some changes to the gl-moveweb command so that it now supports moving a root level web (that is, converting a site collection to a sub-site). Note that there are limitations when converting a site collection to a sub-site. If you are using the fantastic 40 application templates you're likely to run into issues (see the comments for gl-convertsubsitetositecollection). Also - if you are moving a root web (site collection) the -retainobjectidentity parameter will not work (an error will be returned if you attempt to use it).
Last month I created a couple pretty basic commands to help me with setting the navigational elements of my site: "gl-enumnavigation" and "gl-addnavigationnode". You can find information about those commands here: Site Navigation Settings.
As I worked more on my upgraded site I found that what I really needed was a way to make drastic changes using an XML file rather than trying to add or remove one item at a time (note that I haven't created anything to allow removing a single item but I suspect after what I've just done that wouldn't be too hard). To address my needs I first needed to modify my original gl-enumnavigation command so that it could output XML instead of the flat list that it previously did (probably should have done that to begin with but...).
This was pretty easy to do except for one stumbling block - you'd think it would be easy to determine whether a site or page was hidden and that this info would be part of the SPNavigationNode object - unfortunately that's not the case - it took me a bit of digging to realize that I had to "find" the correct PublishingWeb or PublishingPage object and then checks it's IncludeInGlobalNavigation or IncludeInCurrentNavigation properties.
The second thing I needed to do was to create a new command which would be able to take the generated XML from the gl-enumnavigation command and use it to "rebuild" the navigation. This works pretty well in the sense that you can export using gl-enumnavigation, modify the generated XML to meet whatever custom needs you have, and then import using the gl-setnavigationnodes command that I created.
For my purposes this has enabled me to do a test upgrade - work the navigation to how by business users stipulated, save that out to a file and then re-run my upgrade any number of times and simple reset the navigation using the previously generated file. Another use could be to help get around the fact that there's no approval process for navigation changes - an administrator could make the proposed changes in a test site (either in a test farm or on the same production farm), get stakeholder approval, and then import the changes to the production site (and gl-enumnavigation could be used to make a backup of the existing navigation in the event that it becomes necessary to roll-back).
Once I had these two commands created I realized that it wasn't a big step to create a copy command. So I created gl-copynavigation which really just combines gl-enumnavigation and gl-setnavigationnodes but doesn't require you to deal with creating the file (though it does have an option to backup the target in case you want to revert the site). The two commands are detailed below.
I'd like to say that this was real easy to do - it should have been - but as it turned out it was much more difficult than I thought it would be (I'm beginning to detect a trend). I have two main methods which do all the work - the first, SetNavigation() does all the prep and cleanup work; the second, AddNodes() actually does the adding of the nodes to the appropriate collection. The real difficult part of all this was that what you see in the browser is not what you get when you query the GlobalNavigationNodes and CurrentNavigationNodes collections. These collections will not show the various sub-sites and pages that show up in the navigation unless you've explicitly set some property (by moving a node for example). So I had to take look in more than once place for everything and the logic of it all is really bizarre. The code is well documented so I won't go through it again here:
The syntax of the command can be seen below:
C:\>stsadm -help gl-setnavigationnodes stsadm -o gl-setnavigationnodes Rebuilds the Global and/or Current navigation based on a passed in XML file which can be generated by the gl-enumnavigation command and then modified (note that the Id attribute is ignored). Parameters: -url <site collection url> -inputfile <xml file to use as input> [-showsubsites <true (default) | false>] [-showpages <true | false (default)>] [-deleteexistingglobal <true (default) | false>] [-deleteexistingcurrent <true (default) | false>] [-backuptarget <filename>]
Here’s an example of how to set the navigation replacing both the global (Top Nav) and current (Quick Launch) navigation with that specified in the input file "input.xml":
stsadm –o gl-setnavigationnodes –url "http://intranet/hr" -inputfile "c:\input.xml" -backuptarget "c:\backup.xml"
If you want to do more of a merge (add everything from the XML file to the end of the navigation list) you would do the following:
stsadm –o gl-setnavigationnodes –url "http://intranet/hr" -inputfile "c:\input.xml" -backuptarget "c:\backup.xml" -deleteexistingglobal false -deleteexistingcurrent false
Doing the above would be useful if you've got a certain set of navigational elements that you want to have on all your site collections - you could define the XML file once and then use setnavigationnodes to add those elements to each site collection without having to do it manually.
Update 10/15/2007: I enhanced this command slightly. If you add the XML tag "<AutoAddSubSites />" to the source XML the code will replace this with nodes corresponding to all the sub-sites for the target web. This is useful if you've got a global navigation that you want to propagate to various site collections but you want the site collections sub-sites listed in addition to what you are importing. In my case I added the following XML to my source so that all sub-sites would appear under a heading called "Sub Sites":
I basically got this command for free - all I'm doing is utilizing what I'd already done for gl-enumnavigation and gl-setnavigationnodes. I haven't actually needed to use this myself but I thought someone might find it useful and as it was simple to create (which has been a nice change) I didn't really spend much time on it. Here's the core of the code:
As you can see from the above - it's really quite simple. The syntax of the command can be seen below:
C:\>stsadm -help gl-copynavigation stsadm -o gl-copynavigation Copies the Global and/or Current navigation from one site collection to another. Parameters: -sourceurl <source site collection url> -targeturl <target site collection url> [-globalonly / -currentonly] [-showsubsites <true (default) | false>] [-showpages <true | false (default)>] [-deleteexistingglobal <true (default) | false>] [-deleteexistingcurrent <true (default) | false>] [-backuptarget <filename>]
Here’s an example of how to copy both the global (Top Nav) and current (Quick Launch) navigation from the root site collection to a site collection at the managed path "hr":
stsadm –o gl-copynavigation –sourceurl "http://intranet/" -targeturl "http://intranet/hr" -backuptarget "c:\backup.xml"
If you'd like to just copy the global navigation and leave the quick launch (current) then you'd do the following:
stsadm –o gl-copynavigation –sourceurl "http://intranet/" -targeturl "http://intranet/hr" -backuptarget "c:\backup.xml" -globalonly
Setting deleteexistingglobal or deleteexistingcurrent to false would result in the source items being added to the existing navigation rather than replacing the existing navigation.
Update 11/2/2007: I've modified the gl-setnavigationnodes command so that it now allows you to add XML nodes to the passed in XML which will be replaced dynamically. The two nodes are <SiteCollectionUrl /> and <WebUrl />. If either (or both) of these two nodes appear in the XML they will be replaced with the appropriate server relative URL of the specified web or site collection. For example - you could have the following Node element in your XML:
<Node Id="2030" Title="Document Center" IsVisible="True"> <Url><SiteCollectionUrl />/Docs</Url> <vti_navsequencechild>true</vti_navsequencechild> <UrlQueryString></UrlQueryString> <NodeType>Area</NodeType> <Description>Main Document Center for Site Collection</Description> <UrlFragment></UrlFragment> </Node>
Setting the navigation is another common post upgrade/deployment task that should be easy to script. One of the challenges in using the commands I created was that it's necessary to know the node ID of the navigational elements. There's probably ways to change this to use the name rather than the ID but you can run into trouble when dealing with items with the same name. To set the navigation using the web interface go to your top level site ->Site Settings->Navigation.
This one took me a while to figure out as the code that Microsoft wrote to handle this was cryptic at best. It was very difficult to wade through some of the oddness. In the end I figured out that you simply had to get a PublishingWeb object and manipulate the GlobalNavigationNodes collection or the CurrentNavigationNodes collection depending on your intentions. In order to add a new node you need to know the existing node IDs so I created an enumerate command to get that information (again there's probably another way to approach this but this worked for my needs). The two commands I created are called gl-enumnavigation and gl-addnavigationnode.
The code for this turned out to be pretty simple. The main part of the code is a recursive routine which goes through all the nodes and their children. I added some indenting to give it more of a tree layout (UPDATE 9/4/2007: I added an xml switch to allow the output to be specified as XML instead of flat text - the results can also be dumped to an output file specified via an outputfile parameter).
As you can see from the code above, the EnumerateCollection static method does most of the work. It takes in an SPNavigationNodeCollection and iterates through the collection, passing any children back into the same method recursively for processing. The syntax of the command can be seen below:
C:\>stsadm -help gl-enumnavigation stsadm -o gl-enumnavigation Returns the site navigation hierarchy. Parameters: -url <site collection> [-xml] [-outputfile <file to output results to>
Here’s an example of how to return the site navigation:
stsadm –o gl-enumnavigation –url "http://intranet/"
The results of running the above command can be seen below (note that I'm also output all the properties associated with each node object which can be useful when trying to determine how the various NodeType enums are assigned:
C:\>stsadm -o gl-enumnavigation -url "http://intranet/ Global Navigation: - 2018: Topics :NodeType=Area :vti_navsequencechild=true - 2019: News :NodeType=Area :vti_navsequencechild=true - 2021: Sites :NodeType=Area :vti_navsequencechild=true Current Navigation: - 2013: Topics :NodeType=Area :vti_navsequencechild=true - 2014: News :NodeType=Area :vti_navsequencechild=true - 2016: Sites :NodeType=Area :vti_navsequencechild=true
Figuring out how to add a new node took a little more digging. Turns out that you need to use SPNavigationSiteMapNode.CreateSPNavigationNode() which is a static method that returns back an SPNavigationNode object. You pass into this static method the name, link, type, and nodes collection that the new node belongs to (global or current). Once the node is created you set the remaining properties and call Update(). At this point the node exists as the last item - now you simply move it to where you want (no need to update again):
The syntax of the command can be seen below:
C:\>stsadm -help gl-addnavigationnode stsadm -o gl-addnavigationnode Adds a new node to the site hierarchy. Parameters: -url <site collection url> -name <navigation name> -linkurl <link url> -type <link type: None | Area | Page | List | ListItem | PageLayout | Heading | AuthoredLinkToPage | AuthoredLinkToWeb | AuthoredLinkPlain | AuthoredLink | Default | Custom | All> -global / -current -addasfirst / -addaslast / -addafter <previous node id> [-newwindow] [-description <description text>]
Here’s an example of how to add a new node after an existing node:
stsadm –o gl-addnavigationnode–url "http://intranet/" -name "Help Desk" -linkurl "http://helpdesk/" -type AuthoredLinkPlain -global -addafter 2019 -newwindow -description "Corporate Help Desk Application"
Check out the books I've contributed to at Amazon.com:
- PowerShell Cmdlets
- PowerShell Scripting
- STSADM Commands