I've moved my blog to!. 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.

Monday, October 15, 2007

Replace Web Part Content

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>
  12: internal static void ReplaceValues(SPWeb web, SPFile file, Settings settings)
  13: {
  14:  if (file == null)
  15:  {
  16:   return; // This should never be the case.
  17:  }
  19:  SPLimitedWebPartManager manager = web.GetLimitedWebPartManager(file.Url, PersonalizationScope.Shared);
  21:  Log(settings, "Processing File: " + manager.ServerRelativeUrl);
  23:  Regex regex = new Regex(settings.SearchString);
  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:  }
  30:  bool fileModified = false;
  31:  bool wasCheckedOut = true;
  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;
  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 =
  50:      ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut,
  51:           ref modified);
  52:    }
  53:    else if (webPart is PageViewerWebPart)
  54:    {
  55:     PageViewerWebPart wp = webPart as PageViewerWebPart;
  56:     webPart =
  57:      ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut,
  58:           ref modified);
  59:    }
  60:    else if (webPart is ImageWebPart)
  61:    {
  62:     ImageWebPart wp = webPart as ImageWebPart;
  63:     webPart =
  64:      ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut,
  65:           ref modified);
  66:    }
  67:    else if (webPart is SiteDocuments)
  68:    {
  69:     SiteDocuments wp = webPart as SiteDocuments;
  70:     webPart =
  71:      ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut,
  72:           ref modified);
  73:    }
  74:    else if (webPart is SummaryLinkWebPart)
  75:    {
  76:     SummaryLinkWebPart wp = webPart as SummaryLinkWebPart;
  77:     webPart =
  78:      ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut,
  79:           ref modified);
  80:    }
  81:    else if (webPart is ContentByQueryWebPart)
  82:    {
  83:     ContentByQueryWebPart wp = webPart as ContentByQueryWebPart;
  84:     webPart =
  85:      ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut,
  86:           ref modified);
  87:    }
  88:    else if (webPart is DataFormWebPart)
  89:    {
  90:     DataFormWebPart wp = webPart as DataFormWebPart;
  91:     webPart =
  92:      ReplaceValues(web, file, settings, wp, regex, ref manager, ref wasCheckedOut,
  93:           ref modified);
  94:    }
  96:    if (modified && !settings.Test)
  97:     manager.SaveChanges(webPart);
  99:    if (modified)
 100:     fileModified = true;
 101:   }
 103:  }
 105:  if (fileModified)
 106:   file.CheckIn("Checking in changes to list item due to automated search and replace (\"" + settings.SearchString + "\" replaced with \"" + settings.ReplaceString + "\").");
 108:  if (file.InDocumentLibrary && fileModified && settings.Publish && !wasCheckedOut)
 109:   PublishItems.PublishListItem(file.Item, file.Item.ParentList,
 110:           new PublishItems.Settings(settings.Quiet, settings.Test,
 111:                   settings.LogFile),
 112:           "\"stsadm.exe -o replacewebpartcontent\"");
 113:  Log(settings, "Finished Processing File: " + manager.ServerRelativeUrl + "\r\n");
 114: }
 116: /// <summary>
 117: /// Replaces the content of a <see cref="ContentEditorWebPart"/>.
 118: /// </summary>
 119: /// <param name="web">The web that the file belongs to.</param>
 120: /// <param name="file">The file that the web part is associated with.</param>
 121: /// <param name="settings">The settings object containing user provided parameters.</param>
 122: /// <param name="wp">The web part whose content will be replaced.</param>
 123: /// <param name="regex">The regular expression object which contains the search pattern.</param>
 124: /// <param name="manager">The web part manager.  This value may get updated during this method call.</param>
 125: /// <param name="wasCheckedOut">if set to <c>true</c> then the was checked out prior to this method being called.</param>
 126: /// <param name="modified">if set to <c>true</c> then the web part was modified as a result of this method being called.</param>
 127: /// <returns>The modified web part.  This returned web part is what must be used when saving any changes.</returns>
 128: internal static WebPart ReplaceValues(SPWeb web, 
 129:  SPFile file, 
 130:  Settings settings, 
 131:  ContentEditorWebPart wp,
 132:  Regex regex,
 133:  ref SPLimitedWebPartManager manager,
 134:  ref bool wasCheckedOut,
 135:  ref bool modified)
 136: {
 137:  if (wp.Content.FirstChild == null && string.IsNullOrEmpty(wp.ContentLink))
 138:   return wp;
 140:  // The first child of a the content XmlElement for a ContentEditorWebPart is a CDATA section
 141:  // so we want to work with that to make sure we don't accidentally replace the CDATA text itself.
 142:  bool isContentMatch = false;
 143:  if (wp.Content.FirstChild != null)
 144:   isContentMatch = regex.IsMatch(wp.Content.FirstChild.InnerText);
 145:  bool isLinkMatch = regex.IsMatch(wp.ContentLink);
 147:  if (!isContentMatch && !isLinkMatch)
 148:   return wp;
 150:  string content;
 151:  if (isContentMatch)
 152:   content = wp.Content.FirstChild.InnerText;
 153:  else
 154:   content = wp.ContentLink;
 156:  string result = regex.Replace(content, settings.ReplaceString);
 158:  Log(settings,
 159:   string.Format("Match found: File={0}, WebPart={1}, Replacement={2} => {3}",
 160:        file.ServerRelativeUrl, wp.Title, content, result));
 161:  if (!settings.Test)
 162:  {
 163:   if (file.CheckOutStatus == SPFile.SPCheckOutStatus.None)
 164:   {
 165:    file.CheckOut();
 166:    wasCheckedOut = false;
 167:   }
 168:   // We need to reset the manager and the web part because a checkout (now or from an earlier call) 
 169:   // could mess things up so safest to just reset every time.
 170:   manager = web.GetLimitedWebPartManager(file.Url, PersonalizationScope.Shared);
 171:   wp = (ContentEditorWebPart)manager.WebParts[wp.ID];
 173:   if (isContentMatch)
 174:    wp.Content = GetDataAsXmlElement("Content", "", result);
 175:   else
 176:    wp.ContentLink = result;
 178:   modified = true;
 179:  }
 180:  return wp;
 181: }
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.

        [-url <url to search>]
        -searchstring <regular expression string to search for>
        -replacestring <replacement string>
        -scope <Farm | WebApplication | Site | Web | Page>
        [-webpartname <web part name>]
        [-logfile <log file>]
        [-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.


Anonymous said...

Thanks a lot :)
It's just what I was looking for - to replace my List IDS in Dataviews after import.

Anonymous said...

Great commands! I've used several and they have really saved me a lot of time!

Is there a way to convert the "grouped listings" web part created by migration to the summary links web part? Seems like it would be a simple matter of copying links and groups into the new web part using the listings list as the source.. This would be helpful because we want to be able to provide one set of instructions to our users for updating and maintaining those links..

Gary Lapointe said...

I don't have anything that will do that explicitly but you're right - it would be a fairly simple matter to get the list of links and then just update the summary links web part.

jaxstate said...

I am having some trouble accessing the content of the summarylink publishing fields that are included on the WelcomeLinks publishing pages.

I can add and remove links and groups from a summarylinks web part just fine but I have not been able to find a way to manipulate the controls in the "WelcomeLinks" layout page... is this even possible?

Gary Lapointe said...

Yes - it is possible to manipulate this data. It's just stored as a page field (a column or field of the page's list item) and is a pretty simple xml value. If you want to see what the field values look like use the gl-exportlistitem2 command and then you can use something like gl-replacefieldvalues or gl-setmetadata to manipulate that xml.

Emad said...

Thank you very much
This saved me a LOT of time and efforts.
Great tool

zieglers said...

Hi Gary,

First of all, thanks for this magnificient tool.

My question is related to updating List IDs after importing a site.

Generally I develop POCs. I have sort of a template feature for creating publishing sites with different layouts and themes.

Steps I follow are as follow:

1. Create a publishing portal site from template.

2. Enable Publishing site feature (simply creates master pages, page layouts, web-parts)

3. Import exported lists (using gl-import, gl-export for this)

4. Update List IDs in DataForm Web Parts -- right at this point I need your help since I tried to use replacecontentwebpart command, but for this you need to know list ids to be replaced.

In order to update list ids in all web parts (which were created at step 2), what can I do?

Is there any command that I can make use of for this?

Many thanks in advance


Gary Lapointe said...

Zieglers - is this you answering your own question: :)

Anonymous said...

Thank you very much for the nice commands
I have an issue with this command. Whenever I issue STSADM -o gl-replacewebpartcontent -url http://xx/sectors/rooms/Pages/default.aspx -scope page -searchstring "(?" -replacestring "yyy" -publish -logfile "c:\replace.log" -test

I get just the help screen of the stsadm tool.

Is this the correct command to replace the links in a custom link summary web part?

Gary Lapointe said...

Make sure that help for the command is working. Also make sure you're not copying from my blog (the dashes sometimes come through as hyphen character).

chetali said...

Hi Gary,

Can you please share the complete code for this tool?

We have not been able to use Settings parameters mentioned.

Also if you can share some help document on how to use this utility.

chetali said...


I have downloaded the wsp provided.

The command runs successfully but the links are not changed.

Please help!!!

Gary Lapointe said...

What parameters did you use?

chetali said...

you are right. I got the utility working..great piece of work!!!
Just a small problem.. i am not able to update the links in summary link webpart. Any idea???

Gary Lapointe said...

What syntax (parameter values) are you using to update the links?

chetali said...

same parameters as that of content editor webpart...just changing the webpart name

chetali said...

Is it possible for you to share the code for changing links in a asummary link webpart

chetali said...

I am developing my own custom code to update the summary link webpart through code. I am able to successfully check out the page but when I do webPartmanager.SaveChanges(wp); it throws me an exception "The page is not checked out". It will be great if you can help me out in this scenario. Looking forward to your help.

asaf n said...


First of all, this is a great tool.

I'm planning on using it after a big re-arrangement of sites on my portal.(backup and restore them to a different site structure)

I want to know if it's possible to import a file of string and replacement similar to what you update on: gl-replacefildvalues.

because I got allot of changes I want to go over the webparts only one time and update all of the sites.

Thanks In Advance

Gary Lapointe said...

Unfortunately I haven't had time to add that capability in. You might have some success using regular expression groups but I'd be real careful if you go that route.

Mark said...

Hey Gary. Thank you for always being so responsive to comments on your blog posts. These tools you've developed are invaluable beyond words.

I'm having a problem with the command gl-replacewebpartcontent. I'm really trying to create a batch command that will change specific links in a web page, replacing spaces in those known links with hyphens.

I have a CEWP on the page specified in the -url parameter and whenever I execute the command, it says the operation completed successfully but nothing is changed. I've made sure all the hyphens were the correct character but to no avail.

Here's the command that I enter:

stsadm -o gl-replacewebpartcontent -url "http://hostname/media-resources/Pages/news-archive.aspx" -scope page -searchstring "(?i:Media%
20Library/07-1205-Evans Joins Company.pdf)" -replacestring "Media%20Library/07-1
205-Evans-Joins-Company.pdf" -publish -logfile c:\replace.log

Any help would be greatly appreciated.

Gary Lapointe said...

Sorry for not responding sooner - been a busy couple of weeks - are you sure the %20 are stored in the content?

dewing1984 said...

Hi, Gary
I have added a content editor web part on to the document libary page (you know the one when you navigate to a document library and you have the new, upload, actions and settings buttons). This command doesnt seem to be able to update this CEWP. Is this because the web part is not on a "web" and therefore never gets found. I tried looking for the web part using sharepoint manager but couldnt find it in the site with the document library.

Gary Lapointe said...

I can't remember but I believe I'm not iterating through those pages.

Stefan said...

Hi Gary,
I try to replace a part of a link in a webpart containing a question mark and an ampersand (200&?DocumentKey) with a string containing only an ampersand (200?DocumentKey). But the string cannot be found. Is there a problem with specific signs like & and ? ?
How can I search for it?

Thank you in advance

Gary Lapointe said...

Stefan - I use a regular expression to do the searching which means you'll need to escape any special characters such as the question mark (can't remember if the & is one or not) - try adding a backslash in front of each character: 200\&\?DocumentKey.

Anonymous said...

Hi Gary,

I need to do a mass change to replace the "Site Image" web part values for the name of the image to be displayed and the Alternative Text . I was wondering if this command would be able to do this. The data looks like it is stored in an xml format. If this command can not do it then do you have one that will.

Also, replaceing the image content and keeping the established image name is not an option. To be honest this would be fast and easy. But does not change the Alternative Text .

Thanks in advanced for the help.

SoerenNielsen said...

Thanks for the post.

I've stolen a few of your methods and converted them into powershell in my Upgrade script.

I think you'll find this one interesting:

Gary Lapointe said...

I just went to download your script and got a listing of usernames and email addresses instead. BTW - I did migrate the publish items command to a 2010 cmdlet if you'd rather use that.