Experience Editor bug: Multi-line text field deletes extra lines on save

Experience Editor bug: Multi-line text field deletes extra lines on save

There is a pretty annoying bug when editing multi-line text fields through the Experience Editor which has been there since Sitecore 8.1 Update-1 and is not fixed as of 8.1 Update-3.

The issue

If you enter multiple lines into a multi-line text field when editing with the Experience Editor and save, then only the first line is actually saved – all extra lines are deleted.

This happens because of a greedy Regular Expression – <br.*/?> – in the class Sitecore.ExperienceEditor.WebUtility which removes all content between the first <br> and the last > in the content.

Sitecore is aware of the problem and has registered the bug, but they do not have a temporaray fix for it, as they usually have.

The WebUtility class is a static class only meant for internal use by Sitecore and as such is not replacable through config files as most other things are.

You can read a bit more about this issue including another multi-line bug here and here.

A solution/workaround

While WebUtility being static makes the issue a bit more problematic to fix globally it is not impossible to at least workaround the Experience Editor issue.

By creating a fixed version of the relevant method in the WebUtility class and replacing the CallServerSavePipeline processor with our own we can workaround the problem. We get a bit dependent on changes to these parts by Sitecore in future releases, but hopefully this will be fixed in the next release so we don’t need to do this fix.

The code

We start out by creating our fixed version of the WebUtility.GetFields(Database, Dictionary<string, string>) method. I’ve highlighted the interesting part below.

public class WebUtility
{
    public static IEnumerable<PageEditorField> GetFields(Database database, Dictionary<string, string> dictionaryForm)
    {
        Assert.ArgumentNotNull(dictionaryForm, "dictionaryForm");
        var list = new List<PageEditorField>();
        foreach (var key in dictionaryForm.Keys)
        {
            if (!key.StartsWith("fld_", StringComparison.InvariantCulture) &&
                !key.StartsWith("flds_", StringComparison.InvariantCulture))
            {
                continue;
            }

            var text = key;
            var str1 = dictionaryForm[key];
            var indexOfDollarSign = text.IndexOf('$');
            if (indexOfDollarSign >= 0)
            {
                text = StringUtil.Left(text, indexOfDollarSign);
            }
            var values = text.Split('_');
            var itemId = ShortID.DecodeID(values[1]);
            var fieldId = ShortID.DecodeID(values[2]);
            var language = Language.Parse(values[3]);
            var version = Sitecore.Data.Version.Parse(values[4]);
            var revision = values[5];
            var item = database.GetItem(itemId);

            if (item == null)
                continue;

            var field = item.Fields[fieldId];
            if (key.StartsWith("flds_", StringComparison.InvariantCulture))
            {
                str1 = (string)WebUtil.GetSessionValue(str1);
                if (string.IsNullOrEmpty(str1))
                    str1 = field.Value;
            }
            switch (field.TypeKey)
            {
                case "html":
                case "rich text":
                    str1 = str1.TrimEnd(' ');
                    break;
                case "text":
                    str1 = StringUtil.RemoveTags(str1);
                    break;
                // FIX START
                case "multi-line text":
                case "memo":
                    str1 = StringUtil.RemoveTags(new Regex("<br.*?/*>", RegexOptions.IgnoreCase)
                                     .Replace(str1, "\r\n"));
                    break;
                // FIX END
            }
            var pageEditorField = new PageEditorField()
            {
                ControlId = text,
                FieldID = fieldId,
                ItemID = itemId,
                Language = language,
                Revision = revision,
                Value = str1,
                Version = version
            };
            list.Add(pageEditorField);
        }
        return list;
    }
}

Next up we need to create a new processor which gets called when you save through the Experience Editor.

public class CallServerSavePipeline : PipelineProcessorRequest<PageContext>
{
    public override PipelineProcessorResponseValue ProcessRequest()
    {
        var responseValue = new PipelineProcessorResponseValue();
        var pipeline = PipelineFactory.GetPipeline("saveUI");
        pipeline.ID = ShortID.Encode(ID.NewID);
        var saveArgs = RequestContext.GetSaveArgsPatched(); // FIX
        using (new ClientDatabaseSwitcher(RequestContext.Item.Database))
        {
            pipeline.Start(saveArgs);
            CacheManager.GetItemCache(RequestContext.Item.Database).Clear();
            responseValue.AbortMessage = Translate.Text(saveArgs.Error);
            return responseValue;
        }
    }
}

On the highlighted line we call a new extension method on PageContext, which is just a fixed version of the original method GetSaveArgs() except we call our new WebUtility.GetFields() method instead.

public static class PageContextExtensions
{
    public static SaveArgs GetSaveArgsPatched(this PageContext pageContext)
    {
        var args = PipelineUtil.GenerateSaveArgs(pageContext.Item,
            WebUtility.GetFields(pageContext.Item.Database, pageContext.FieldValues), // FIX
            "",
            pageContext.LayoutSource,
            "",
            Sitecore.ExperienceEditor.WebUtility.GetCurrentLayoutFieldId().ToString());
        args.HasSheerUI = false;
        new ParseXml().Process(args);
        return args;
    }
}

At last all we need to do is to replace the processor with our new one.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <sitecore.experienceeditor.speak.requests>
      <request name="ExperienceEditor.Save.CallServerSavePipeline"
               set:type="YourAsembly.CallServerSavePipeline, YourAssembly"/>
    </sitecore.experienceeditor.speak.requests>
  </sitecore>
</configuration>

That’s it! Now your multi-line text fields will work correctly when editing them in the Experience Editor. Hopefully Sitecore will fix this soon in the core product.