Fixing absolute URLs for all Alternate Access Mappings (AAM) of Content Editor Web Part with a Control Adapter

Hi everyone and sorry for the no-post for several months.  My ‘Draft’ list keeps piling but I didn’t take the time to unpile it much.

 

Anyhow, I came across this problem again last week with the out of the box Content Editor Web Part (CEWP).  Basically, if you use the rich html text editing capability, all URLs are absolute. 

 

I came across a similar issue last year while using the HtmlField and Content Deployment where the URLs were still pointing to the authoring web site.  The CEWP was also renowned to have the same issue.  Back then, we had fixed the general issue with a quick HttpModule installing an HttpFilter which was essentially rewriting URLs.  In terms of architecture, I didn’t like the idea but it was a quick fix done in the early hours.  It did the job.  Since then, we removed Content Deployment so we didn’t have the issue anymore, thus we removed the HttpModule.

 

While adding new Alternate Access Mappings URLs to the web site, we noticed some link errors with only the CEWP this time.  I looked at past support cases to see if the issue had been resolved in any Cumulative Update (up to December 2008) but no, it’s not.  Apparently, the fix involves so many changes in the source code that it’s too high a risk.  As of right now, it’s not scheduled to be fixed.  The official solutions were :

  • Update your CEWP for Publishing Html Fields.  Anyone doing publishing knows that it’s not possible in all scenarios.  If you know all the Html content types, using Html fields is better and recommended, however, in most landing pages, it’s difficult to do.
  • After using the Rich Editing capability, go in the source code and update all the links manually.  This is beyond practical that I won’t discuss it.
  • Look at 3rd party components.  The only one I found that was readily SharePoint enabled was the one from Telerik : RadEditor.  It’s a great control that I would recommend for whatever else it does or if you start a new web site.  Unfortunately, the Lite (and free) edition doesn’t seem to support the properties StripAbsoluteAnchorPaths and StripAbsoluteImagesPaths.  The complete version allows you to use these and it’s 1,000$ USD per developer.

While I’d definitely recommend the RadEditor control, it wasn’t fixing my currently existing pages that uses the CEWP.  My next thought was back to the HttpModule but then I remembered looking at ASP.NET Control Adapters. 

 

ASP.NET Control Adapters is basically a way to modify the Render of a control.  It’s a little bit like inheritance but only for Render AND it allows to inherit Sealed class such as the ContentEditorWebPart class.  Since I was a bit rusted with Control Adapters, I typed in a search for “ContentEditorWebPart ASP.NET Control Adapter” and came across this post from Waldek Mastykarz.  He lays out most of the essential but he essentially strips all absolute URLs and this was impractical in my scenario.

 

In most scenario, you’ll want to strip a list of URLs.  In the case of Content Deployment, you’ll want to supply the list of AAMs from the authoring environment (if it’s in the same farm, you could do this programmatically much like what I’m doing in this code but you supply a different URL to the SPWebApplication.Lookup method, if it’s a different farm, supply the list in a config file).  In the case of a single site with multiple AAMs, this code will work great:

    1: using System;
    2: using System.Collections.Generic;
    3: using System.Text;
    4: using System.Web;
    5: using System.Web.UI;
    6: using System.Web.UI.WebControls;
    7: using System.Web.UI.Adapters;
    8: using System.IO;
    9: using System.Text.RegularExpressions;
   10: using Microsoft.SharePoint.Administration;
   11:  
   12:  
   13: namespace MaximeBBlog
   14: {
   15:     public class ContentEditorWebPartAdapter : ControlAdapter
   16:     {
   17:         protected override void Render(System.Web.UI.HtmlTextWriter writer)
   18:         {
   19:             StringBuilder sb = new StringBuilder();
   20:             HtmlTextWriter htw = new HtmlTextWriter(new StringWriter(sb));
   21:             base.Render(htw);
   22:             string output = sb.ToString();
   23:  
   24:             List<Uri> alternateUrls = GetAlternateUrls();
   25:             foreach (Uri alternateUrl in alternateUrls)
   26:             {
   27:                 output = output.Replace(alternateUrl.ToString(), "/");
   28:             }
   29:  
   30:             writer.Write(output);
   31:         }
   32:  
   33:         private List<Uri> GetAlternateUrls()
   34:         {
   35:             List<Uri> alternateUrls = (List<Uri>)HttpContext.Current.Cache["AlternateUrls"];
   36:             if (alternateUrls == null)
   37:             {
   38:                 alternateUrls = new List<Uri>();
   39:  
   40:                 SPWebApplication webApp = SPWebApplication.Lookup(System.Web.HttpContext.Current.Request.Url);
   41:                 foreach (SPAlternateUrl alternateUrl in webApp.AlternateUrls)
   42:                 {
   43:                     alternateUrls.Add(alternateUrl.Uri);
   44:                 }
   45:  
   46:                 HttpContext.Current.Cache.Add("AlternateUrls", alternateUrls, null, DateTime.Now.AddHours(12), System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal, null);
   47:             }
   48:  
   49:             return alternateUrls;
   50:         }
   51:     }
   52: }

 

In a nutshell, I’m doing the following:

  1. Create a class inheriting from System.Web.UI.Adapters.ControlAdapter
  2. Override the Render method and read the stream’s text (Html)
  3. Get the list of AlternateUrls from the current Web Application
  4. Keep the list in caching (I did it for 12 absolute hours, you can change that to your liking)
  5. Do a simple String.Replace() of the AlternateUrls for a relative url

 

In terms of performance, it’s negligible and it won’t be reprocessed if you have Page Output Caching enabled.  The SPWebApplication.Lookup is also very fast and I’m keeping the list in cache. As for the architecture, it ensures you are only stripping the correct URLs for only that control.

 

You’ll notice that there is no mention of the ContentEditorWebPart so far and that’s because we aren’t inheriting this Web Part.  That’s where the Control Adapter magic starts and you could even attach it to multiple controls if there’s a need!   To bind it, you need to add (or update if you have one already) a compat.browser file in the App_Browsers folder of your web site.  This file requires the following:

 

    1: <browsers>
    2:     <browser refID="Default">
    3:         <controlAdapters>
    4:             <adapter controlType="Microsoft.SharePoint.WebPartPages.ContentEditorWebPart" adapterType="MaximeBBlog.ContentEditorWebPartAdapter, MaximeBBlog, Version=1.0.0.0, Culture=neutral, PublicKeyToken=febc2e2cb2c3d564" />
    5:         </controlAdapters>
    6:     </browser>
    7: </browsers>

Note: All you need to update is the namespace in both the code and compat.browser file to match your own; as well as signing it with your key.

 

You’ll notice that the DLL is signed, this allows me to deploy it in the GAC (through a WSP for example) and NOT to require Full trust.  I’m still using WSS_Minimal in fact.  There is NO requirement for a SafeControl entry in the Web.Config either.  Once your DLL is in the GAC and the compat.browser file in the App_Browsers directory of your web site, that web site will render the ContentEditorWebPart through your class.

 

Note: if you application domain is already loaded, you may need to recycle it for the update to start working, especially if the page was in the page output already.

 

Thanks again to Waldek for his early start on the issue.

 

Cheers!