Allowing web editors to customize static error pages in EPiServer

Some time back, I encountered the need to dynamically alter static error pages through EPiServer’s edit mode; or more specifically, edit an error page for a 500 Internal Server Error, caused by a database related failure. Since it might be tiresome for editors to change static files on a webserver, and since EPiServer tends to enjoy having a database connection retrieving pages, I needed to do something different.

What I came up with was a solution writing the whole page to a static file when the web editor published a new version, by using a fake HttpWorkerRequest. The complete code for this is available at the bottom of this post.

I started by setting up a few basic things, such as a page type for the error page, using PageTypeBuilder. There is nothing special about this; just added a property to allow editors to write formatted content.

StaticErrorPage.cs

[PageType("7C6EA22F-3F3A-4986-8B0B-4A352F27B3B1",
    Filename = "~/CodeSample/StaticError.aspx",
    Name = "Error page")]
public class StaticErrorPage : TypedPageData
{
    [PageTypeProperty(
      Type = typeof (PropertyXhtmlString),
      EditCaption = "Error message")]
    public virtual string ErrorMessage { get; set; }
}

For this to work, there is also need for a fake identity as well as a fake principal; extending the IIdentity and IPrincipal interfaces. These are only for creating dummy instances, so just override the necessary properties to make the compiler happy, and we will get back to them in a moment.

FakeIdentity.cs

public class FakeIdentity : IIdentity
{
    public string Name {get{return string.Empty;}}
    public string AuthenticationType {get{return string.Empty;}}
    public bool IsAuthenticated {get{return false;}}
}

FakePrincipal.cs

public class FakePrincipal : IPrincipal
{
    public bool IsInRole(string role) {return false;}
    public IIdentity Identity {get{return new FakeIdentity();}}
}

There are only a few methods and properties that we need to care about for our fake HttpWorkerRequest. Have a look in the source code if you are interested to see which ones; the interesting parts are shown in the snippet below.

FakeHttpWorkerRequest.cs

public TextWriter OutputTextWriter { get; set; }
public override void SendResponseFromMemory(byte[] data, int length)
{
    OutputTextWriter.Write(Encoding.UTF8.GetChars(data, 0, length));
}

The OutputTextWriter is the source from which we will get the captured HTML code after faking the page request, and the SendResponseFromMemory method is what is filling it with information. You will find an included MasterPage file along with a small style sheet in the source code. This should probably be a special version of your real MasterPage, stripped of all functionality requiring the database.

The first thing that we need to do in the StaticError page template is creating a publish event and trigger it when the web editor decides to publish the page.

StaticError.aspx.cs

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
    DataFactory.Instance.PublishedPage += OnPublishedPage_PublishStaticVersion;
}

private void OnPublishedPage_PublishStaticVersion(object sender, PageEventArgs e)
{
    const string outputFile = @"C:\EPiServer\MyEPiServerSite\error.html";
    var page = (StaticErrorPage)e.Page;
    using (var streamWriter = File.CreateText(outputFile))
    {
        streamWriter.WriteLine(GenerateStaticHtml(page.PageLink));
        streamWriter.Flush();
    }
}

You will probably want to set another file output path in some other manner, but this filled my sample code purpose. The code is pretty straight forward; use a StreamWriter to create the file and call the GenerateStaticHtml method with the PageReference to the page being published. Make sure that you have the proper permissions to where you are trying to place the file.

private string GenerateStaticHtml(PageReference pageRef)
{
    var pageData = GetPage(pageRef);
    var uri = new Uri(Configuration.Settings.Instance.SiteUrl, pageData.LinkURL);
    var workerRequest = new FakeHttpWorkerRequest
        {
            ApplicationPhysicalPath = HttpContext.Current.Request.PhysicalApplicationPath,
            ApplicationVirtualPath = Configuration.Settings.Instance.SiteUrl.LocalPath,
            PageVirtualPath = pageVirtualPath,
            QueryString = uri.Query,
            OutputTextWriter = stringWriter
        };

The GenerateStaticHtml method itself is where all the fun happens. Creating an Uri object from the address (37) saves us a lot work as there is no need for manual string parsing. Create a fake worker request using available data, and include a writer object for accessing the captured HTML.

var realHttpContext = HttpContext.Current;
HttpContext.Current = new HttpContext(workerRequest)
                            {
                                User = new FakePrincipal()
                            };

As we would very much like to keep our real HttpContext once we are done faking, we safetly put it away in a temporary variable before replacing it with a new one. As you can see, we instantiate it using our fake principal as user. If we would use the real one instead, EPiServer would believe that our fake request comes from a logged in web editor; as that is exactly what we are when we are publishing pages. This would result in unnecessary style sheets and JavaScripts, handling for instance the EPiServer Context Menu (right-click menu in view mode), to be injected into the error page HTML. *

var pageFileName = uri.Segments[uri.Segments.Length - 1];
var pagePhysicalPath = HttpContext.Current.Server.MapPath(pageFileName);
HttpContext.Current.Handler = PageParser.GetCompiledPageInstance(pageVirtualPath, pagePhysicalPath, HttpContext.Current);
HttpContext.Current.Handler.ProcessRequest(HttpContext.Current);
HttpContext.Current.Response.Flush();
HttpContext.Current = realHttpContext;

The last element in the Uri-Segments array (57) contains the name of the template file; in this case StaticError.aspx. We will need the physical path to this file in order to create a handler (59) for our fake context. After having it successfully processing the request we can safetly switch back to our real HttpContext (62).

var externalUrl = new UrlBuilder(uri);
Global.UrlRewriteProvider.ConvertToExternal(externalUrl, pageData.PageLink, System.Text.Encoding.UTF8);
var htmlRewriter = Global.UrlRewriteProvider.GetHtmlRewriter();
var internalUrl = new UrlBuilder(uri);
var html = htmlRewriter.RewriteString(internalUrl, externalUrl, System.Text.Encoding.UTF8, stringWriter.GetStringBuilder().ToString());

The final part of the method will be dealing with path structure. Since the brand new static error.html file might not be placed at the same location as the page template, for instance style sheets and JavaScripts may not be found. The chunk of code above simply uses the EPiServer HTML rewriter functionality making sure that all linked content is accessible.

Of course, this will not allow web editors to make use of uploaded images from the VPP-folders, but it will give them an opportunity update their static files. I have a few ideas about dealing with this drawback, so who knows, maybe it will make it to later post.

* The EPiServer ContextMenu can be switched off by adding a bit of code to the page template. However, I still feel it is better to pretend to be a visitor as that is who the error pages really are for.

StaticError.aspx.cs – Snippet not included in sample code

    public partial class StaticError : TemplatePage<StaticErrorPage>
    {
        public StaticError()
            : base(0, EPiServer.Web.PageExtensions.ContextMenu.OptionFlag)
        {         
        }

Source code

Source code: StaticErrorPages.zip
GitHub-link: https://github.com/matkun/Blog/tree/master/SamplesAndExamples/StaticEPiServerErrorPages