Thursday, 15 May 2014

The Sitecore Insert Options Loophole

As you probably know, Sitecore content editors are evil geniuses. They masquerade as reasonable, hard working professionals, but don't be fooled. The spend their days hatching diabolical schemes to uncover any remaining weaknesses in the platform, ready to gleefully spring them on you last thing on a Friday. In the last year alone I've had the following phone conversation more times than I care to remember:

Me: Hi. How can I help?
User: I added a News page to the Case Study section of our site and it's not appearing.
Me: Oh, I thought I'd configured Sitecore's Insert Options to prevent that.
User: Yes you did, but I'm an evil genius.
Me: Sorry, I'm not sure I follow.
User: Yes, I just created the page in the News section and then used "Move To".
Me: I see. How come you need News pages in the Case Study section?
User: I don't, but Sitecore let me to do it, which means it's a bug. Mwhahaha!
This is a common tactic that your average evil genius uses to exploit the fact that Sitecore doesn't enforce Insert options for the "Move To" or "Copy To" commands. Luckily, Sitecore uses a pipeline to service them both so we can easily add in the extra functionality.

The following code is the prototype for a class that I intend to include in every fresh Sitecore install from now on. The class is added to both the "uiCopyItems" and "uiMoveItems" pipelines, and checks if the item you're source item violates the target item's Insert Options.
public class InsertOptionsCheck
{
    // The code used for both pipelines is the same except for the  
    // naming of a single parameter in the args object. uiCopyItems  
    // uses "destination" and uiMoveItems uses "target".
        
    public void ProcessCopy(ClientPipelineArgs args)
    {
        Process(args, "destination");
    }

    public void ProcessMove(ClientPipelineArgs args)
    {
        Process(args, "target");
    }

    private void Process(ClientPipelineArgs args, String paramName)
    {
        Assert.ArgumentNotNull(args, "args");

        // If you see "Insert From Template" when you use the
        // Insert menu, then you can move or copy anything.
        if (HasInsertFromTemplatePermissions())
            return;

        Database db = Factory.GetDatabase(args.Parameters["database"]);
        Assert.IsNotNull(db, "db");

        Item targetItem = GetTargetItem(args, paramName, db);
        Assert.IsNotNull(targetItem, "targetItem");

        List<Item> sourceItems = GetSourceItems(args, db);

        var permitted = GetPermittedTemplateIds(targetItem, db);
        var attempted = GetAttemptedTemplateIds(sourceItems, db);
            
        // If the all attempted items are allowed by the permitted 
        // Insert Options, then everything is ok.
        if (!attempted.Except(permitted).Any())
            return;

        // If you've got this far, then you've 
        // been trying to cheat the system
        args.AbortPipeline();
        SheerResponse.Alert("Your evil plan was thwarted!", new String[0]);     
    }

    private bool HasInsertFromTemplatePermissions()
    {
        // We know if they have permission by checking if they can access
        // the "Insert From Template" item in the core database

        Database coreDb = Factory.GetDatabase("core");
        Assert.IsNotNull(coreDb, "coreDb");
        String insertFromTemplateId = "{F300EB7B-82ED-4E43-817C-6327E9BD1BD6}";
        var item = coreDb.GetItem(insertFromTemplateId);

        return item != null;     
    }

    private Item GetTargetItem(ClientPipelineArgs args, String name, Database db)
    {
        // This is where the pipelines differ. The ID of the target 
        // item is stored in differently named parameters.

        var targetId = args.Parameters[name];
        Assert.IsNotNullOrEmpty(targetId, "id");
        var targetItem = db.GetItem(targetId);
        return targetItem;
    }

    private List<Item> GetSourceItems(ClientPipelineArgs args, Database db)
    {
        var sourceIds = args.Parameters["items"].Split('|').ToList();
        Assert.IsTrue(sourceIds.Any(), "sourceIds.Any()");
        var sourceItems = sourceIds.Select(id => db.GetItem(id)).ToList();
        return sourceItems;
    }

    private List<String> GetPermittedTemplateIds(Item targetItem, Database db)
    {
        // We use the same method that Sitecore uses to get the 
        // Insert Options - GetMasters. 'Master' is an obsolete 
        // term, but it remains in use in the API. 

        List<Item> templates = Masters.GetMasters(targetItem);
        var templateIds = templates.Select(x => x.ID.ToString());
        return templateIds.ToList();
    }

    private List<String> GetAttemptedTemplateIds(List<Item> items, Database db)
    {          
        var templateIds = items.Select(item => item.TemplateID.ToString());
        return templateIds.ToList();
    }
}
Add this config file to your App_Config/Include folder to make sure the code runs at the right point in each pipeline (the name of the config file isn't important).
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      
      <uiMoveItems>
        <processor 
        patch:before="*[@method='Execute']" 
        mode="on"
        type="Loopholes.InsertOptionsCheck, Loopholes" 
        method="ProcessMove" />
      </uiMoveItems>
      
      <uiCopyItems>
        <processor
        patch:before="*[@method='Execute']"
        mode="on"
        type="Loopholes.InsertOptionsCheck, Loopholes"
        method="ProcessCopy" />
      </uiCopyItems>    
      
    </processors>
  </sitecore>
</configuration>

That's it. Evil geniuses will never stop scheming, but hopefully this post will help you to thwart their plans at least temporarily.