MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Sunday, January 18, 2009

Replacing Navigation URLs Using STSADM

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

9 comments:

rcd said...

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.

tripwire said...

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.

Gary Lapointe said...

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.

Montanan Missing her Mountains said...

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!

Gary Lapointe said...

Retype the command - you've got long hyphens in there.

Montanan Missing her Mountains said...

that fixed it!! Thanks Gary - you just saved us beaucoup de hours!

Anonymous said...

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

Anonymous said...

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?

Gary Lapointe said...

This command won't help with URLs in content but the gl-replacewebpartcontents and gl-replacefieldvalues should do what you want.