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
2: using System;
3: using System.Collections.Generic;
4: using System.Collections.Specialized;
5: using System.Text;
6: using System.Text.RegularExpressions;
7: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
8: using Microsoft.SharePoint;
9: using Microsoft.SharePoint.Administration;
10: using Microsoft.SharePoint.Navigation;
11: using Microsoft.SharePoint.Publishing;
12: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
13: using Microsoft.SharePoint.Publishing.Navigation;
14:
15: namespace 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 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)"
-search "(?i:/doccenter/IT)" |
replacestring | replace | Yes | The replace string. | -replacestring "/docs/IT"
-replace "/docs/IT" |
quiet | q | No | Specify to suppress status information while the command is running. | -quiet
-q |
scope | s | No – defaults to site | The 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
14 thoughts on “Replacing Navigation URLs Using STSADM”
The command works, but it seems to remove any items under the current Nav headings. So, before running, under “Documents” I see “Shared Documents”. After running, the URL for Documents is corrected, but “Shared Documents” has been removed. Seems to happen for each Nav heading.
Hi Gary, can you provide a bit more insight into the searchsting sytnax? Does it accept all regex formats?
i.e. ?/groups/ict -> teams/ict
I got an unrecogised construct error on something quit esimple.
I’m bascially just doing a Regex.Replace (http://msdn.microsoft.com/en-us/library/xwewhkd1.aspx) so anything you can do with that should work.
Gary,
Your extensions are a life saver. Anywho – I’m trying to use this and getting a malfunction somewhere in my syntax:
stsadm -o gl-replacenavigationurls –url http://sample.company.com/client/AAMESDRAPE –searchstring (?i:http://library.company.com/template) –replacestring http://library.company.com/AAMESDRAPE –scope WebApplication
Any suggestions where I am going awry?
thanks!
Retype the command – you’ve got long hyphens in there.
that fixed it!! Thanks Gary – you just saved us beaucoup de hours!
Hi Gary,
Thanks for your toolpack. I have a problem when using the gl-replacenavigationurl command. I get an access denied. Why? The currently logged in user is a farm administrator.
The esp deployment went well. What do I wrong?
The exact error message is:
Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
Hi gray
I would like to know if I could change the severer name in ulrs with this extension ?
My problem is about old urls in site content.I have added a content database from another server.The top urls work fine but the images and and the links which are in site content haven’t changed and it referes to the old server.
Any idea?
This command won’t help with URLs in content but the gl-replacewebpartcontents and gl-replacefieldvalues should do what you want.
I’d love to have a non-destructive flag to see what the command would change if run. Any chance that could be added? Thanks!
Yup. That would be awesome if that could be added 🙂
It’s just a question of time – right now I don’t have it – plus there’s issues with this as there are cases where I have to create new nodes and if I don’t comit them then child items will fail so a test is tough to get 100% right.
Hi Gary,
Great extension, I love it. It works flawlessy on our site except for the folders inside a list. Items in the root of the list are affected but the folders and the items inside it are not affected. Is there a workaround?
Does this work with sharepoint 2010 ? I’m trying this and I get a command line error
Trying this using
stsadm -o gl-replacenavigationurls –url “http://sharepoint01/it” –searchstring “http://elink.test.com” –replacestring “http://sharepoint01” –scope WebApplication