If you’ve read some of my earlier posts you’ll know that I’ve been working on 3 commands to enable replacing content throughout the site in order to deal with the broken links I’ve encountered as a result of moving so many sites around for our upgrade.

The first command was gl-replacefieldvalues and the second command was gl-applyupgradeareaurlmappings. This third command, gl-replacewebpartcontent, is focused on replacing content in various built-in web part types. The code for the command, and even the usage of the command, is similar in many respects to the gl-replacefieldvalues command. There are a series of methods used to handle the different scope options that can be passed in and then a primary method which handles the processing of the various files.

Once the web parts have been identified on the page this primary method calls another method based on the web part type. These methods that take in a web part as a parameter are fairly similar in structure with the core difference being what properties are evaluated and modified as each web part type is considered independently. I’m currently only considering the following web parts: ContentEditorWebPart, PageViewerWebPart, ImageWebPart, SiteDocuments, SummaryLinkWebPart, DataFormWebPart, and ContentByQueryWebPart. The last two are only really helpful when the value you are replacing is a List ID – I use the methods that handle these two web part types in my gl-repairsitecollectionimportedfromsubsite command, gl-convertsubsitetositecollection command, and my gl-moveweb command in order to retarget web parts that were hard coded to point to a specific List ID which would have changed as a result of the move.

There’s quite a bit of code for this command but most of it is more or less cookie cutter so I’ll only show a portion of it. The following code shows the primary decision maker method which determines which web part method should be called. I’ve also included one method showing how I handle the ContentEditorWebPart – remember that each web part will have it’s own way of setting its content.

  1/// <summary>
  2/// Replaces the content of the various web parts on a given page (file).  This is the main
  3/// decision maker method which calls the various worker methods based on web part type.
  4/// The following web part types are currently supported (all others will be ignored):
  5/// <see cref="ContentEditorWebPart"/>, <see cref="PageViewerWebPart"/>, <see cref="ImageWebPart"/>,
  6/// <see cref="SiteDocuments"/>, <see cref="SummaryLinkWebPart"/>, <see cref="DataFormWebPart" />,
  7/// <see cref="ContentByQueryWebPart"/>
  8/// </summary>
  9/// <param name="web">The web to which the file belongs.</param>
 10/// <param name="file">The file containing the web parts to search.</param>
 11/// <param name="settings">The settings object containing user provided parameters.</param>
 12internal static void ReplaceValues(SPWeb web, SPFile file, Settings settings)
 13{
 14    if (file == null)
 15    {
 16        return; // This should never be the case.
 17    }
 18
 19    SPLimitedWebPartManager manager = web.GetLimitedWebPartManager(file.Url, PersonalizationScope.Shared);
 20
 21    Log(settings, "Processing File: " + manager.ServerRelativeUrl);
 22
 23    Regex regex = new Regex(settings.SearchString);
 24
 25    if (file.InDocumentLibrary && Utilities.IsCheckedOut(file.Item) && !Utilities.IsCheckedOutByCurrentUser(file.Item))
 26    {
 27        return; // The item is checked out by a different user so leave it alone.
 28    }
 29
 30    bool fileModified = false;
 31    bool wasCheckedOut = true;
 32
 33    SPLimitedWebPartCollection webParts = manager.WebParts;
 34    for (int i = 0; i < webParts.Count; i++)
 35    {
 36        WebPart webPart = webParts[i] as WebPart;
 37        if (webPart == null)
 38            continue;
 39
 40        bool modified = false;
 41        string webPartName = webPart.Title.ToLowerInvariant();
 42        if (settings.WebPartName == null || settings.WebPartName.ToLowerInvariant() == webPartName)
 43        {
 44            // As every web part has different requirements we are only going to consider a small subset.
 45            // Custom web parts will not be addressed as there's no interface that can utilized.
 46            if (webPart is ContentEditorWebPart)
 47            {
 48                ContentEditorWebPart wp = webPart as ContentEditorWebPart;
 49                webPart = ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut, ref modified);
 50            }
 51            else if (webPart is PageViewerWebPart)
 52            {
 53                PageViewerWebPart wp = webPart as PageViewerWebPart;
 54                webPart = ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut, ref modified);
 55            }
 56            else if (webPart is ImageWebPart)
 57            {
 58                ImageWebPart wp = webPart as ImageWebPart;
 59                webPart = ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut, ref modified);
 60            }
 61            else if (webPart is SiteDocuments)
 62            {
 63                SiteDocuments wp = webPart as SiteDocuments;
 64                webPart = ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut, ref modified);
 65            }
 66            else if (webPart is SummaryLinkWebPart)
 67            {
 68                SummaryLinkWebPart wp = webPart as SummaryLinkWebPart;
 69                webPart = ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut, ref modified);
 70            }
 71            else if (webPart is ContentByQueryWebPart)
 72            {
 73                ContentByQueryWebPart wp = webPart as ContentByQueryWebPart;
 74                webPart = ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut, ref modified);
 75            }
 76            else if (webPart is DataFormWebPart)
 77            {
 78                DataFormWebPart wp = webPart as DataFormWebPart;
 79                webPart = ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut, ref modified);
 80            }
 81
 82            if (modified && !settings.Test)
 83                manager.SaveChanges(webPart);
 84
 85            if (modified)
 86                fileModified = true;
 87        }
 88
 89    }
 90
 91    if (fileModified)
 92        file.CheckIn("Checking in changes to list item due to automated search and replace (\"" + settings.SearchString + "\" replaced with \"" + settings.ReplaceString + "\").");
 93
 94    if (file.InDocumentLibrary && fileModified && settings.Publish && !wasCheckedOut)
 95        PublishItems.PublishListItem(file.Item, file.Item.ParentList,
 96            new PublishItems.Settings(settings.Quiet, settings.Test, settings.LogFile),
 97            "\"stsadm.exe -o replacewebpartcontent\"");
 98    Log(settings, "Finished Processing File: " + manager.ServerRelativeUrl + "\r\n");
 99}
100 
101/// <summary>
102/// Replaces the content of a <see cref="ContentEditorWebPart"/>.
103/// </summary>
104/// <param name="web">The web that the file belongs to.</param>
105/// <param name="file">The file that the web part is associated with.</param>
106/// <param name="settings">The settings object containing user provided parameters.</param>
107/// <param name="wp">The web part whose content will be replaced.</param>
108/// <param name="regex">The regular expression object which contains the search pattern.</param>
109/// <param name="manager">The web part manager.  This value may get updated during this method call.</param>
110/// <param name="wasCheckedOut">if set to <c>true</c> then the was checked out prior to this method being called.</param>
111/// <param name="modified">if set to <c>true</c> then the web part was modified as a result of this method being called.</param>
112/// <returns>The modified web part.  This returned web part is what must be used when saving any changes.</returns>
113internal static WebPart ReplaceValues(SPWeb web, 
114    SPFile file, 
115    Settings settings, 
116    ContentEditorWebPart wp,
117    Regex regex,
118    ref SPLimitedWebPartManager manager,
119    ref bool wasCheckedOut,
120    ref bool modified)
121{
122    if (wp.Content.FirstChild == null && string.IsNullOrEmpty(wp.ContentLink))
123        return wp;
124
125    // The first child of a the content XmlElement for a ContentEditorWebPart is a CDATA section
126    // so we want to work with that to make sure we don't accidentally replace the CDATA text itself.
127    bool isContentMatch = false;
128    if (wp.Content.FirstChild != null)
129        isContentMatch = regex.IsMatch(wp.Content.FirstChild.InnerText);
130    bool isLinkMatch = regex.IsMatch(wp.ContentLink);
131
132    if (!isContentMatch && !isLinkMatch)
133        return wp;
134
135    string content;
136    if (isContentMatch)
137        content = wp.Content.FirstChild.InnerText;
138    else
139        content = wp.ContentLink;
140
141    string result = regex.Replace(content, settings.ReplaceString);
142
143    Log(settings, string.Format("Match found: File={0}, WebPart={1}, Replacement={2} => {3}", file.ServerRelativeUrl, wp.Title, content, result));
144    if (!settings.Test)
145    {
146        if (file.CheckOutStatus == SPFile.SPCheckOutStatus.None)
147        {
148            file.CheckOut();
149            wasCheckedOut = false;
150        }
151        // We need to reset the manager and the web part because a checkout (now or from an earlier call) 
152        // could mess things up so safest to just reset every time.
153        manager = web.GetLimitedWebPartManager(file.Url, PersonalizationScope.Shared);
154        wp = (ContentEditorWebPart)manager.WebParts[wp.ID];
155
156        if (isContentMatch)
157            wp.Content = GetDataAsXmlElement("Content", "http://schemas.microsoft.com/WebPart/v2/ContentEditor", result);
158        else
159            wp.ContentLink = result;
160
161        modified = true;
162    }
163    return wp;
164}

The syntax of the command can be seen below:

C:\>stsadm -help gl-replacewebpartcontent

stsadm -o gl-replacewebpartcontent

Replaces all occurances of the search string with the replacement string.  Supports use of regular expressions.  Use -test to verify your replacements before executing.

Parameters:
        [-url <url to search>]
        -searchstring <regular expression string to search for>
        -replacestring <replacement string>
        -scope <Farm | WebApplication | Site | Web | Page>
        [-webpartname <web part name>]
        [-quiet]
        [-test]
        [-logfile <log file>]
        [-publish]
        [-unsafexml (treats known XML data as a string)]

As with the other content replacement commands that I created I strongly suggest that you run the command in test mode (by passing in the -test parameter) before executing the command for real. This will enable you to verify all replacements that will occur before letting them actually happen.

Here’s an example of how to replace all occurances of the search string “/divisions/humanresources/” (case insensitive) with “/hr/” for all web parts within the “intranet” web application:

stsadm –o gl-replacewebpartcontent -url "http://intranet" -scope webapplication -searchstring "(?i:/divisions/humanresources/)" -replacestring "/hr/" -publish -logfile "c:\replace.log"

You can also pass in a web part name and it will only make changes to web parts that match the specific name (probably not very useful with such a broad scope).

Update 11/28/2007: I’ve added a new parameter, -unsafexml, which if passed in will result in replacement code treating known XML data as a flat string rather than attempting to load the XML into an XmlDocument object and replacing just text data. This was necessary due to the fact that Microsoft stores invalid XML in the DataFormWebPart’s ParameterBindings property (Microsoft is doing a lot of funky stuff to get around the fact that they are doing this and it wasn’t something that I felt safe about reproducing so I decided to just add this flag so that if you knew your replacement was safe then you’d still be able to use the command). At this point only the DataFormWebPart is affected by this parameter. As always, make sure you use the -test parameter before running without it.