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):

  1#if MOSS
  2using System;
  3using System.Collections.Generic;
  4using System.Collections.Specialized;
  5using System.Text;
  6using System.Text.RegularExpressions;
  7using Lapointe.SharePoint.STSADM.Commands.SPValidators;
  8using Microsoft.SharePoint;
  9using Microsoft.SharePoint.Administration;
 10using Microsoft.SharePoint.Navigation;
 11using Microsoft.SharePoint.Publishing;
 12using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
 13using Microsoft.SharePoint.Publishing.Navigation;
 14 
 15namespace Lapointe.SharePoint.STSADM.Commands.SiteCollectionSettings
 16{
 17    public class ReplaceNavigationUrls : SPOperation
 18    {
 19        private static Regex m_searchString;
 20        private static string m_replaceString;
 21 
 22        /// <summary>
 23        /// Initializes a new instance of the <see cref="ReplaceNavigationUrls"/> class.
 24        /// </summary>
 25        public ReplaceNavigationUrls()
 26        {
 27            SPParamCollection parameters = new SPParamCollection();
 28            parameters.Add(new SPParam("url", "url", true, null, new SPUrlValidator(), "Please specify the site collection."));
 29            parameters.Add(new SPParam("scope", "s", false, "site", new SPRegexValidator("(?i:^WebApplication$|^Site$|^Web$)")));
 30            parameters.Add(new SPParam("searchstring", "search", true, null, new SPNonEmptyValidator(), "Please specify the search string."));
 31            parameters.Add(new SPParam("replacestring", "replace", true, null, new SPNullOrNonEmptyValidator(), "Please specify the replace string."));
 32            parameters.Add(new SPParam("quiet", "q"));
 33 
 34            StringBuilder sb = new StringBuilder();
 35            sb.Append("\r\n\r\nReplaces URL values in the current and global navigation matching the provided search pattern.\r\n\r\nParameters:");
 36            sb.Append("\r\n\t-url <url to search>");
 37            sb.Append("\r\n\t-searchstring <regular expression string to search for>");
 38            sb.Append("\r\n\t-replacestring <replacement string>");
 39            sb.Append("\r\n\t[-quiet]");
 40            sb.Append("\r\n\t[-scope <WebApplication | Site | Web> (defaults to Site)]");
 41 
 42            Init(parameters, sb.ToString());
 43        }
 44 
 45        #region ISPStsadmCommand Members
 46 
 47        /// <summary>
 48        /// Gets the help message.
 49        /// </summary>
 50        /// <param name="command">The command.</param>
 51        /// <returns></returns>
 52        public override string GetHelpMessage(string command)
 53        {
 54            return HelpMessage;
 55        }
 56 
 57        /// <summary>
 58        /// Runs the specified command.
 59        /// </summary>
 60        /// <param name="command">The command.</param>
 61        /// <param name="keyValues">The key values.</param>
 62        /// <param name="output">The output.</param>
 63        /// <returns></returns>
 64        public override int Execute(string command, StringDictionary keyValues, out string output)
 65        {
 66            output = string.Empty;
 67 
 68            Verbose = !Params["quiet"].UserTypedIn;
 69            string url = Params["url"].Value.TrimEnd('/');
 70            string scope = Params["scope"].Value.ToLowerInvariant();
 71            m_searchString = new Regex(Params["searchstring"].Value);
 72            m_replaceString = Params["replacestring"].Value;
 73 
 74            Log("Start Time: {0}", DateTime.Now.ToString());
 75            SPEnumerator en;
 76            switch (scope)
 77            {
 78                case "webapplication":
 79                    en = new SPEnumerator(SPWebApplication.Lookup(new Uri(url)));
 80                    en.SPWebEnumerated += SPWebEnumerated;
 81                    en.Enumerate();
 82                    break;
 83                case "site":
 84                    using (SPSite site = new SPSite(url))
 85                    {
 86                        en = new SPEnumerator(site);
 87                        en.SPWebEnumerated += SPWebEnumerated;
 88                        en.Enumerate();
 89                    }
 90                    break;
 91                case "web":
 92                    using (SPSite site = new SPSite(url))
 93                    using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
 94                    {
 95                        en = new SPEnumerator(web);
 96                        en.SPWebEnumerated += SPWebEnumerated;
 97                        en.Enumerate();
 98                    }
 99                    break;
100            }
101            Log("Finish Time: {0}\r\n", DateTime.Now.ToString());
102            
103            return OUTPUT_SUCCESS;
104        }
105 
106        #endregion
107 
108        /// <summary>
109        /// Handles the enumerated event for each web within the scope.
110        /// </summary>
111        /// <param name="sender">The sender.</param>
112        /// <param name="e">The <see cref="Lapointe.SharePoint.STSADM.Commands.OperationHelpers.SPEnumerator.SPWebEventArgs"/> instance containing the event data.</param>
113        private static void SPWebEnumerated(object sender, SPEnumerator.SPWebEventArgs e)
114        {
115            Log("Progress: Processing \"{0}\".", e.Web.Url);
116 
117            if (PublishingWeb.IsPublishingWeb(e.Web))
118            {
119                PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(e.Web);
120                ReplaceUrls(e.Web, pubweb.GlobalNavigationNodes, true);
121                ReplaceUrls(e.Web, pubweb.CurrentNavigationNodes, false);
122            }
123            else
124            {
125                ReplaceUrls(e.Web, e.Web.Navigation.GlobalNodes, true);
126                ReplaceUrls(e.Web, e.Web.Navigation.TopNavigationBar, true);
127                ReplaceUrls(e.Web, e.Web.Navigation.QuickLaunch, false);
128            }
129        }
130 
131        /// <summary>
132        /// Replaces the urls within each node in the collection.
133        /// </summary>
134        /// <param name="web">The web.</param>
135        /// <param name="nodes">The nodes.</param>
136        /// <param name="isGlobal">if set to <c>true</c> [is global].</param>
137        private static void ReplaceUrls(SPWeb web, SPNavigationNodeCollection nodes, bool isGlobal)
138        {
139            if (nodes == null || nodes.Count == 0)
140                return;
141 
142            List<SPNavigationNode> toUpdate = new List<SPNavigationNode>();
143            foreach (SPNavigationNode node in nodes)
144            {
145                if (m_searchString.IsMatch(node.Url))
146                    toUpdate.Add(node);
147 
148                ReplaceUrls(web, node.Children, isGlobal);
149            }
150 
151            foreach (SPNavigationNode node in toUpdate)
152            {
153                string result = m_searchString.Replace(node.Url, m_replaceString);
154 
155                Log("Progress: Replacing \"{0}\" with \"{1}\".", node.Url, result);
156 
157 
158                NodeTypes type = NodeTypes.None;
159                if (node.Properties["NodeType"] != null && !string.IsNullOrEmpty(node.Properties["NodeType"].ToString()))
160                    type = (NodeTypes)Enum.Parse(typeof(NodeTypes), node.Properties["NodeType"].ToString());
161 
162                if (type == NodeTypes.Area || 
163                    type == NodeTypes.Page || 
164                    type == NodeTypes.None || 
165                    type == NodeTypes.List ||
166                    type == NodeTypes.ListItem ||
167                    type == NodeTypes.Heading)
168                {
169                    CreateNode(web, node, result, nodes, isGlobal);
170                }
171                else
172                {
173                    string oldUrl = node.Url;
174                    node.Url = result;
175                    try
176                    {
177                        node.Update();
178                    }
179                    catch
180                    {
181                        //Console.WriteLine("New Url={0}, Type={2}, Children={1}", node.Url, node.Children.Count, node.Properties["NodeType"]);
182                        node.Url = oldUrl;
183                        CreateNode(web, node, result, nodes, isGlobal);
184                    }
185                }
186            }
187        }
188 
189        /// <summary>
190        /// Creates the node.
191        /// </summary>
192        /// <param name="web">The web.</param>
193        /// <param name="sourceNode">The source node.</param>
194        /// <param name="url">The URL.</param>
195        /// <param name="nodes">The nodes.</param>
196        /// <param name="isGlobal">if set to <c>true</c> [is global].</param>
197        private static void CreateNode(SPWeb web, SPNavigationNode sourceNode, string url, SPNavigationNodeCollection nodes, bool isGlobal)
198        {
199            NodeTypes type = NodeTypes.None;
200            if (sourceNode.Properties["NodeType"] != null && !string.IsNullOrEmpty(sourceNode.Properties["NodeType"].ToString()))
201                type = (NodeTypes)Enum.Parse(typeof(NodeTypes), sourceNode.Properties["NodeType"].ToString());
202 
203            NodeTypes newType = type;
204            if (type == NodeTypes.Area)
205                newType = NodeTypes.AuthoredLinkToWeb;
206            else if (type == NodeTypes.Page)
207                newType = NodeTypes.AuthoredLinkToPage;
208            else if (type == NodeTypes.List || type == NodeTypes.ListItem)
209                newType = NodeTypes.AuthoredLink;
210 
211            SPNavigationNode newNode = SPNavigationSiteMapNode.CreateSPNavigationNode(
212                        sourceNode.Title, url, newType, nodes);
213 
214            newNode.Properties["CreatedDate"] = sourceNode.Properties["CreatedDate"];
215            newNode.Properties["LastModifiedDate"] = sourceNode.Properties["LastModifiedDate"];
216            newNode.Properties["Description"] = sourceNode.Properties["Description"];
217            newNode.Properties["Target"] = sourceNode.Properties["Target"];
218 
219            newNode.Update();
220 
221            newNode.Move(nodes, sourceNode);
222 
223            Hide(web, sourceNode, type, isGlobal);
224        }
225 
226        /// <summary>
227        /// Hides the specified pub web.
228        /// </summary>
229        /// <param name="web">The pub web.</param>
230        /// <param name="node">The node.</param>
231        /// <param name="type">The type.</param>
232        /// <param name="isGlobal">if set to <c>true</c> [is global].</param>
233        private static void Hide(SPWeb web, SPNavigationNode node, NodeTypes type, bool isGlobal)
234        {
235            if (type == NodeTypes.Area)
236            {
237                SPWeb childWeb = null;
238                string name = node.Url.Trim('/');
239                if (name.Length != 0 && name.IndexOf("/") > 0)
240                {
241                    name = name.Substring(name.LastIndexOf('/') + 1);
242                }
243                try
244                {
245                    childWeb = web.Webs[name];
246                }
247                catch (ArgumentException)
248                {
249                }
250 
251                if (childWeb != null && childWeb.Exists && childWeb.ServerRelativeUrl.ToLower() == node.Url.ToLower() && PublishingWeb.IsPublishingWeb(childWeb))
252                {
253                    PublishingWeb tempPubWeb = PublishingWeb.GetPublishingWeb(childWeb);
254                    if (isGlobal)
255                        tempPubWeb.IncludeInGlobalNavigation = false;
256                    else
257                        tempPubWeb.IncludeInCurrentNavigation = false;
258                    tempPubWeb.Update();
259                }
260                else
261                {
262                    try
263                    {
264                        node.Delete();
265                    }
266                    catch (SPException)
267                    {
268                    }
269                }
270            }
271            else if (type == NodeTypes.Page)
272            {
273                PublishingPage page = null;
274                try
275                {
276                    if (PublishingWeb.IsPublishingWeb(web))
277                    {
278                        PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web);
279                        page = pubWeb.GetPublishingPages()[node.Url];
280                    }
281                    else
282                    {
283                        try
284                        {
285                            node.Delete();
286                        }
287                        catch (SPException)
288                        {
289                        }
290                    }
291                }
292                catch (ArgumentException)
293                {
294                }
295                if (page != null)
296                {
297                    if (isGlobal)
298                        page.IncludeInGlobalNavigation = false;
299                    else
300                        page.IncludeInCurrentNavigation = false;
301                    page.Update();
302                }
303            }
304            else
305                node.Delete();
306        }
307    }
308}
309#endif

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 NameAvailabilityBuild Date
gl-replacenavigationurlsMOSS 2007Released: 1/18/2009
Parameter NameShort FormRequiredDescriptionExample Usage
urlYesURL of the web application or site collection.-url "http://portal"
searchstringsearchYesThe regular expression search string.-searchstring "(?i:/doccenter/IT)", -search "(?i:/doccenter/IT)"
replacestringreplaceYesThe replace string.-replacestring "/docs/IT", -replace "/docs/IT"
quietqNoSpecify to suppress status information while the command is running.-quiet, -q
scopesNo – defaults to siteThe scope to use. Valid values are “WebApplication”, “Site”, and “Web”.-scope site , -s 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