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