Sitecore EXM – Adjusting how personalization tokens are rendered

For a specific scenario i wanted to be able to pass HTML through a personalization token to be used from within an EXM email. Unfortunately (due to security) personalization tokens are HTML encoded and this behaviour isn’t configurable.

The token replacement is done from within the Sitecore.Modules.EmailCampaign.Core.Personalization.PersonalizationManager class, which is mostly static, private, and totally not extendable. Unfortunately this class does not have an interface, is not injected using dependency injection, and also isn’t constructed using the Sitecore Factory.

This means that we sort of have no other option then decompile the class, copy the code to our own class, and change all the methods that create an instance of the object, to make them construct our own class. There’s a Stack Overflow question about it here, which also has some code examples. As I really detest decompiling code, copying it to my own class and making adjustments, i looked for a different solution. Here’s where the awesome C# reflection capabilities come in.

The handling of replacing tokens in text is done by the PersonalizationManager.ResolveAndModifyTokenHandler delegate, and i’m particularly interested in changing only that method.

As you can see it’s private readonly static member, however in .NET Framework < 5.0, we can set the value with reflection. When using .NET Framework 5+ or .NET Core 2+ this will throw an exception. As long as Sitecore EXM is not targetted for .NET 5.0, this method will continue to work. In the meantime i hope they’ll fix the code so that it’s injected with DI.

How to implement this

We add a processing to the Initialization pipeline that sets the value of the ResolveAndModifyTokenHandler. In the example below I’ve changed the code to not HtmlEncode if the token starts with unencoded_.

using System;
using System.Collections.Generic;
using System.Net;
using System.Reflection;
using Sitecore.Modules.EmailCampaign.Core.Personalization;
using Sitecore.Pipelines;

namespace Example
{
    public class ChangeCustomPersonalizationManager
    {
        public void Process(PipelineArgs args)
        {
            var handlerProperty = typeof(PersonalizationManager).GetField("ResolveAndModifyTokenHandler", BindingFlags.NonPublic | BindingFlags.Static);
            var newDelegate = Delegate.CreateDelegate(handlerProperty.FieldType, typeof(ChangeCustomPersonalizationManager), nameof(ResolveAndModifyTokenHandler));
            handlerProperty.SetValue(null, newDelegate);
        }

        private static string ResolveAndModifyTokenHandler(string text, string key, IList<TokenMapper> tokenMappers,
            ref bool moveStartIndex)
        {
            TokenValue tokenValue = null;
            if (!string.IsNullOrWhiteSpace(key))
            {
                tokenValue = typeof(PersonalizationManager).GetMethod("ResolveAndModifyTokenValue", BindingFlags.NonPublic | BindingFlags.Static)
                    .Invoke(null, new object[] { new Token(key), tokenMappers }) as TokenValue;
            }

            if (tokenValue != null)
            {
                var str = tokenValue.Value == null ? string.Empty : tokenValue.Value.ToString();
                if (!key.ToLowerInvariant().StartsWith("unencoded_"))
                {
                    str = WebUtility.HtmlEncode(str);
                }
                text = text.Replace("$" + key + "$", str);
            }
            else
            {
                moveStartIndex = true;
            }

            return text;
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:x="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:exmEnabled="http://www.sitecore.net/xmlconfig/exmEnabled/" xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore exmEnabled:require="yes" role:require="Standalone or ContentManagement or DedicatedDispatch">
    <pipelines>
      <initialize>
        <processor type="Example.ChangeCustomPersonalizationManager, Example" patch:after="*[last()]"/>
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Leave a Reply

Your email address will not be published.