Home » 2013 » September

Monthly Archives: September 2013

Advertisements

Periodically Unlock Items of Idle Users in Sitecore

In my last post I showed a way to unlock locked items of a user when he/she logs out of Sitecore.

I wrote that article to help out the poster of this thread in one of the forums on SDN.

In response to my reply in that thread — I had linked to my previous post in that reply — John West, Chief Technology Officer of Sitecore, had asked whether we would also want to unlock items of users whose sessions had expired, and another SDN user had alluded to the fact that my solution would not unlock items for users who had closed their browser sessions instead of explicitly logging out of Sitecore.

Immediate after reading these responses, I began thinking about a supplemental solution to unlock items for “idle” users — users who have not triggered any sort of request in Sitecore after a certain amount of time.

I first began tinkering with the idea of using the last activity date/time of the logged in user — this is available as a DateTime in the user’s MembershipUser instance via the LastActivityDate property.

However — after reading this article — I learned this date and time does not mean what I thought it had meant, and decided to search for another way to ascertain whether a user is idle in Sitecore.

After some digging around, I discovered Sitecore.Web.Authentication.DomainAccessGuard.Sessions in Sitecore.Kernel.dll — this appears to be a collection of sessions in Sitecore — and immediately felt elated as if I had just won the lottery. I decided to put it to use in the following class (code in this class will be invoked via a scheduled task in Sitecore):

using System;
using System.Collections.Generic;
using System.Web.Security;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Security.Accounts;
using Sitecore.Tasks;
using Sitecore.Web.Authentication;

namespace Sitecore.Sandbox.Tasks
{
    public class UnlockItemsTask
    {
        private static readonly TimeSpan ElapsedTimeWhenIdle = GetElapsedTimeWhenIdle();

        public void UnlockIdleUserItems(Item[] items, CommandItem command, ScheduleItem schedule)
        {
            if (ElapsedTimeWhenIdle == TimeSpan.Zero)
            {
                return;
            }

            IEnumerable<Item> lockedItems = GetLockedItems(schedule.Database);
            foreach (Item lockedItem in lockedItems)
            {
                UnlockIfApplicable(lockedItem);
            }
        }
        
        private static IEnumerable<Item> GetLockedItems(Database database)
        {
            Assert.ArgumentNotNull(database, "database");
            return database.SelectItems("fast://*[@__lock='%owner=%']");
        }

        private void UnlockIfApplicable(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (!ShouldUnlockItem(item))
            {
                return;
            }
            
            Unlock(item);
        }

        private static bool ShouldUnlockItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if(!item.Locking.IsLocked())
            {
                return false;
            }

            string owner = item.Locking.GetOwner();
            return !IsUserAdmin(owner) && IsUserIdle(owner);
        }

        private static bool IsUserAdmin(string username)
        {
            Assert.ArgumentNotNullOrEmpty(username, "username");
            User user = User.FromName(username, false);
            Assert.IsNotNull(user, "User must be null due to a wrinkle in the interwebs :-/");
            return user.IsAdministrator;
        }

        private static bool IsUserIdle(string username)
        {
            Assert.ArgumentNotNullOrEmpty(username, "username");
            DomainAccessGuard.Session userSession = DomainAccessGuard.Sessions.Find(session => session.UserName == username);
            if(userSession == null)
            {
                return true;
            }

            return userSession.LastRequest.Add(ElapsedTimeWhenIdle) <= DateTime.Now;
        }

        private void Unlock(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            try
            {
                string owner = item.Locking.GetOwner();
                item.Editing.BeginEdit();
                item.Locking.Unlock();
                item.Editing.EndEdit();
                Log.Info(string.Format("Unlocked {0} - was locked by {1}", item.Paths.Path, owner), this);
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }
        }

        private static TimeSpan GetElapsedTimeWhenIdle()
        {
            TimeSpan elapsedTimeWhenIdle;
            if (TimeSpan.TryParse(Settings.GetSetting("UnlockItems.ElapsedTimeWhenIdle"), out elapsedTimeWhenIdle))
            {
                return elapsedTimeWhenIdle;
            }
            
            return TimeSpan.Zero;
        }
    }
}

Methods in the class above grab all locked items in Sitecore via a fast query, and unlock them if the users of each are not administrators, and are idle — I determine this from an idle threshold value that is stored in a custom setting (see the patch configuration file below) and the last time the user had made any sort of request in Sitecore via his/her DomainAccessGuard.Session instance from Sitecore.Web.Authentication.DomainAccessGuard.Sessions.

If a DomainAccessGuard.Session instance does not exist for the user — it’s null — this means the user’s session had expired, so we should also unlock the item.

I’ve also included code to log which items have been unlocked by the Unlock method — for auditing purposes — and of course log exceptions if any are encountered — we must do all we can to support our support teams by capturing information in log files :).

I then created a patch configuration file to store our idle threshold value — I’ve used one minute here for testing (I can’t sit around all day waiting for items to unlock ;)):

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <setting name="UnlockItems.ElapsedTimeWhenIdle" value="00:00:01:00" />
    </settings>
  </sitecore>
</configuration> 

I then created a task command for the class above in Sitecore:

unlock-items-task-command

I then mapped the task command to a scheduled task (to learn about scheduled tasks, see John West’s blog post where he discusses them):

unlock-items-scheduled-task

Let’s light the fuse on this, and see what it does.

I logged into Sitecore using one of my test accounts, and locked some items:

locked-some-items

I then logged into Sitecore using a different account in a different browser session, and navigated to one of the locked items:

mike-locked-items

I then walked away, made a cup of coffee, returned, and saw this:

items-unlocked

I opened up my latest Sitecore log, and saw the following:

unlocked-log

I do want to caution you from running off with this code, and putting it into your Sitecore instance(s) — it is an all or nothing solution (it will unlock items for all non-adminstrators which might invoke some anger in users, and also defeat the purpose of locking items in the first place), so it’s quite important that a business decision is made before using this solution, or one that is similar in nature.

If you have any thoughts on this, please leave a comment.

Advertisements

Unlock Sitecore Users’ Items During Logout

The other day I saw a post in one of the SDN forums asking how one could go about building a solution to unlock items locked by a user when he/she logs out of Sitecore.

What immediately came to mind was building a new processor for the logout pipeline — this pipeline can be found at /configuration/sitecore/processors/logout in your Sitecore instance’s Web.config — but had to research how one would programmatically get all Sitecore items locked by the current user.

After a bit of fishing in Sitecore.Kernel.dll and Sitecore.Client.dll, I found a query in Sitecore.Client.dll that will give me all locked items for the current user:

fast-query-locked-items

Now all we need to do is add it into a custom logout pipeline processor:

using System;
using System.Collections.Generic;
using System.Linq;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.Logout;

namespace Sitecore.Sandbox.Pipelines.Logout
{
    public class UnlockMyItems
    {
        public void Process(LogoutArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            UnlockMyItemsIfAny();
        }

        private void UnlockMyItemsIfAny()
        {
            IEnumerable<Item> lockedItems = GetMyLockedItems();
            if (!CanProcess(lockedItems))
            {
                return;
            }

            foreach (Item lockedItem in lockedItems)
            {
                Unlock(lockedItem);
            }
        }

        private static IEnumerable<Item> GetMyLockedItems()
        {
            return Context.ContentDatabase.SelectItems(GetMyLockedItemsQuery());
        }

        private static string GetMyLockedItemsQuery()
        {
            return string.Format("fast://*[@__lock='%\"{0}\"%']", Context.User.Name);
        }

        private static bool CanProcess(IEnumerable<Item> lockedItems)
        {
            return lockedItems != null
                    && lockedItems.Any()
                    && lockedItems.Select(item => item.Locking.HasLock()).Any();
        }

        private void Unlock(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (!item.Locking.HasLock())
            {
                return;
            }

            try
            {
                item.Editing.BeginEdit();
                item.Locking.Unlock();
                item.Editing.EndEdit();
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }
        }
    }
}

The class above grabs all items locked by the current user in the context content database. If none are found, we don’t move forward on processing.

When there are locked items for the current user, the code checks to see if each item is locked before unlocking, just in case some other account unlocks the item before we unlock it — I don’t know what would happen if we try to unlock an item that isn’t locked. If you know, please share in a comment.

I then injected the above pipeline processor into the logout pipeline using the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <logout>
        <processor patch:after="*[@type='Sitecore.Pipelines.Logout.CheckModified, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.Logout.UnlockMyItems, Sitecore.Sandbox"/>
      </logout>
    </processors>
  </sitecore>
</configuration>

Let’s test-drive this.

I first logged into Sitecore using my ‘mike’ account, and chose the Home item to lock:

lets-lock-home-item

It is now locked:

home-is-locked-1

In another session, I logged in using another account, and saw that ‘mike’ had locked the Home item:

home-is-locked-2

I switched back to the other session under the ‘mike’ user, and logged out:

IF

When I logged back in, I saw that the Home item was no longer locked:

item-now-unlocked

If you have any thoughts or suggestions on making this better, please share in a comment below.

Expand Tokens on Sitecore Items Using a PowerShell Function in Sitecore PowerShell Extensions

During my Sitecore from the Command Line presentation at the Sitecore User Group – New England, I had briefly showcased a custom PowerShell function that expands Sitecore tokens in fields of a supplied item, and how I had saved this function into the Functions section of the Script Library — /sitecore/system/Modules/PowerShell/Script Library/Functions — of the Sitecore PowerShell Extensions module. This blog post captures what I had shown.

This is the custom function I had shown — albeit I changed its name to adhere to the naming convention in PowerShell for functions and commands (Verb-SingularNoun):

function Expand-SitecoreToken {
	<#
        .SYNOPSIS
             Expand tokens on the supplied item
              
        .EXAMPLE
            Expand tokens on the home item.
             
            PS master:\> Get-Item "/sitecore/content/home" | Expand-SitecoreToken
    #>
	[CmdletBinding()]
    param( 
		[ValidateNotNull()]
		[Parameter(ValueFromPipeline=$True)]
        [Sitecore.Data.Items.Item]$item
    )
	
    $item.Editing.BeginEdit()
    
    Try
    {
        $tokenReplacer = [Sitecore.Configuration.Factory]::GetMasterVariablesReplacer()
        $tokenReplacer.ReplaceItem($item)
        $result = $item.Editing.EndEdit()
        "Expanded tokens on item " + $item.Paths.Path
    }
    Catch [system.Exception]
    {
        $item.Editing.CancelEdit()
        "Failed to expand tokens on item"
        "Reason: " + $error
    }
}

The function above calls Sitecore.Configuration.Factory.GetMasterVariablesReplacer() for an instance of the MasterVariablesReplacer class — which is defined and can be overridden in the “MasterVariablesReplacer” setting in your Sitecore instance’s Web.config — and passes the item supplied to the function to the MasterVariablesReplacer instance’s ReplaceItem() method after the item has been put into editing mode.

Once tokens have been expanded, a confirmation message is sent to the Results window.

If an exception is caught, we display it — the exception is captured in the $error global variable.

I saved the above function into the Script Library of my copy of the Sitecore PowerShell Extensions module:

spe-save-function

An item was created in the Script Library to house the function:

Expand-SitecoreToken-item

Let’s try it out.

Let’s expand tokens on the Home item:

spe-home-unexpanded-tokens

In the Integrated Scripting Environment of the Sitecore PowerShell Extensions module, I typed in the following code:

Execute-Script "master:/system/Modules/PowerShell/Script Library/Functions/Expand-SitecoreToken"
Get-Item . | Expand-SitecoreToken

You can consider the Execute-Script “master:/system/Modules/PowerShell/Script Library/Functions/Expand-SitecoreToken” line of code to be comparable to a javascript “script” tag — it will execute the script thus defining the function so we can execute it.

I then ran that code above:

excuted-Expand-SitecoreToken-on-home

Once the script finished running, I went back over to the Content Editor, and saw that tokens were expanded on the Home item:

spe-function-home-tokens-expanded

You might be thinking “Mike, I really don’t want to be bothered with expanding these tokens, and would rather have our Content Editors/Authors do it. is there something we can set up to make that happen?”

You bet. 🙂

In the Sitecore PowerShell Extension module, you can save PowerShell into the Script Library to be executed via an item context menu option click. All you have to do is save it into the Script Library under the Content Editor Context Menu item:

spe-context-menu-option

The script is then saved in a new item created under Content Editor Context Menu item in the Script Library:

spe-content-menu-option-item

Let’s see it in action.

I chose the following page at random to expand tokens:

spe-inner-page-three-unexpanded-tokens

I right-clicked on the item to launch it’s context menu, opened up scripts, and saw a new “Expand Tokens” option:

spe-new-context-menu-option

I clicked it, and was given a dialog with a status bar:

spe-context-menu-expanding-tokens

I refreshed the item, and saw that all tokens were expanded:

spe-context-menu-tokens-expanded

If you have any thoughts or questions on this, please leave a comment.

Until next time, have a scriptabulous day!

Expand Tokens on Sitecore Items Using a Custom Command in Sitecore PowerShell Extensions

During my Sitecore from the Command Line presentation at the Sitecore User Group – New England, I had shown attendees how they could go about adding a custom command into the Sitecore PowerShell Extensions module.

This blog post shows what I had presented — although the code in this post is an improved version over what I had presented at my talk. Many thanks to Sitecore MVP Adam Najmanowicz for helping me make this code better!

The following command will expand “out of the box” tokens in all fields of a supplied Sitecore item — check out Expand Tokens on Sitecore Items Using a Custom Command in Revolver where I discuss the problem commands like this address, and this article by Sitecore MVP Jens Mikkelsen which lists “out of the box” tokens available in Sitecore:

using System;
using System.Management.Automation;

using Sitecore.Configuration;

using Sitecore.Data;
using Sitecore.Data.Items;

using Cognifide.PowerShell.PowerShellIntegrations.Commandlets;

namespace CommandLineExtensions.PowerShell.Commandlets
{
    [Cmdlet("Expand", "Token")]
    [OutputType(new[] { typeof(Item) })]
    public class ExpandTokenCommand : BaseCommand
    {
        private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer();

        [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)]
        public Item Item { get; set; }

        protected override void ProcessRecord()
        {
            Item.Editing.BeginEdit();
            try
            {
                TokenReplacer.ReplaceItem(Item);
                Item.Editing.EndEdit();
            }
            catch (Exception ex)
            {
                Item.Editing.CancelEdit();
                throw ex;
            }

            WriteItem(Item);
        }
    }
}

The command above subclasses Cognifide.PowerShell.PowerShellIntegrations.Commandlets.BaseCommand — the base class for most (if not all) commands in Sitecore PowerShell Extensions.

An item is passed to the command via a parameter, and is magically set on the Item property of the command class instance.

The ValueFromPipeline parameter being set to “true” on the Item property’s Parameter attribute will allow for chaining of this command with others so that items can be fed into it via a pipe bridging the commands together in PowerShell.

An instance of the Sitecore.Data.MasterVariablesReplacer class — which is created by the GetMasterVariablesReplacer() method of the Sitecore.Configuration.Factory class based on the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — is used to expand tokens on the supplied Sitecore item after the item was flagged for editing.

Once tokens have been expanded on the item — or not in the event an exception is encountered — the item is written to the Results window via the WriteItem method which is defined in the BaseCommand class.

I then had to wire up the custom command via a patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <powershell>
      <commandlets>
		    <add Name="Custom Commandlets" type="*, CommandLineExtensions" />
      </commandlets>
    </powershell>
  </sitecore>                                                                                     
</configuration>

Let’s take this custom command for a spin.

I created a bunch of test items, and set tokens in their fields. I then selected the following page at random for testing:

page-one-unexpanded-tokens

I opened up the Integrated Scripting Environment of Sitecore PowerShell Extensions, typed in the following PowerShell code, and executed by pressing Ctrl-E:

spe-ise-expand-tokens-page-one

As you can see tokens were expanded on the Page One item:

page-one-expanded-tokens

How about expanding tokens on all descendants of the Home item? Let’s see an example of how we can do that.

I chose the following content item — a grandchild of the Home item — for testing:

inner-page-one-unexpanded-tokens

I switched back over to the Integrated Scripting Environment, wrote the following code for testing — the Get-ChildItem command with the -r parameter (this means do this recursively) will grab all descendants of the Home item, and pipe each item in the result set into the Expand-Token command — and clicked the Execute button:

spe-ise-expand-on-descendants

I then went back to the grandchild item of the Home page in the content tree, and saw that tokens were expanded in its fields:

spe-expanded-on-descendants

If you have any thoughts or comments on this, or ideas for new commands in Sitecore PowerShell Extensions, please share in a comment.

Until next time, have a scriptolicious day!

Expand Tokens on Sitecore Items Using a Custom Command in Revolver

On September 18, 2013, I presented Sitecore from the Command Line at the Sitecore User Group – New England.

During my presentation, I gave an example of creating a custom command in Revolver — the first scripting platform for Sitecore built by Alistair Deneys — and thought I would write something up for those who had missed the presentation, or wanted to revisit what I had shown.

One thing that plagues some Sitecore developers — if you disagree please leave a comment — is not having a nice way to expand tokens on items when tokens are added to Standard Values after items had been created previously.

Newly added tokens “bleed” into preexisting items’ fields, and I’ve seen developers perform crazy feats of acrobatic gymnastics to expand them — writing a standalone web form to recursive crawl the content tree to expand these is such an example (take a look at Empower Your Content Authors to Expand Standard Values Tokens in the Sitecore Client where I offer an alternative way to expand tokens on content items).

The following custom Revolver command will expand tokens on a supplied Sitecore item, and help out on the front of expanding newly added tokens on preexisting items:

using System;
using Sitecore.Configuration;
using System.Linq;

using Sitecore.Data;
using Sitecore.Data.Items;

using Revolver.Core;
using Revolver.Core.Commands;

namespace CommandLineExtensions.Revolver.Commands
{
    public class ExpandTokensCommand : BaseCommand
    {
        private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer();

        public override string Description()
        {
            return "Expand tokens on an item";
        }

        public override HelpDetails Help()
        {
            HelpDetails details = new HelpDetails
            {
                Description = Description(),
                Usage = "<cmd> [path]"
            };
            
            details.AddExample("<cmd>");
            details.AddExample("<cmd> /item1/item2");
            return details;
        }

        public override CommandResult Run(string[] args)
        {
            string path = string.Empty;
            if (args.Any())
            {
                path = args.FirstOrDefault();
            }

            using (new ContextSwitcher(Context, path))
            {
                if (!Context.LastGoodPath.EndsWith(path, StringComparison.CurrentCultureIgnoreCase))
                {
                    return new CommandResult
                    (
                        CommandStatus.Failure, 
                        string.Format("Failed to expand tokens on item {0}\nReason:\n\n An item does not exist at that location!", path)
                    );
                }

                CommandResult result;
                Item item = Context.CurrentItem;
                item.Editing.BeginEdit();
                try
                {
                    TokenReplacer.ReplaceItem(item);
                    result = new CommandResult(CommandStatus.Success, string.Concat("Expanded tokens on item ", Context.LastGoodPath));
                    item.Editing.EndEdit();
                }
                catch (Exception ex)
                {
                    item.Editing.CancelEdit();
                    result = new CommandResult(CommandStatus.Failure, string.Format("Failed to expand tokens on item {0}\nReason:\n\n{1}", path, ex));
                }

                return result;
            }
        }
    }
}

Tokens are expanded using an instance of the Sitecore.Data.MasterVariablesReplacer class — you can roll your own, and wire it up in the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — which is provided by Sitecore.Configuration.Factory.GetMasterVariablesReplacer().

All custom commands in Revolver must implement the Revolver.Core.ICommand interface. I subclassed Revolver.Core.Commands.BaseCommand — which does implement this interface — since it seemed like the right thing to do given that all “out of the box” commands I saw in Revolver were subclassing it, and then implemented the Description(), Help() and Run() abstract methods.

I then had to bind the custom command to a new name — I chose “et” for “Expand Tokens”:

@echooff
@stoponerror

bind CommandLineExtensions.Revolver.Commands.ExpandTokensCommand,CommandLineExtensions et

@echoon

Since it wouldn’t be efficient to type and run this bind script every time I want to use the “et” command, I added it into a startup script in the core database:

bind-custom-commands-script

I then had to create a user script for the startup script to run. I chose the Everyone role here for demonstration purposes:

bind-custom-commands-user-script

The above startup script will be invoked when Revolver is opened, and our custom command will be bound.

Let’s see all of the above in action.

I added some tokens in my home item:

home-item-unexpanded-tokens

I then opened up Revolver, navigated to /sitecore/content, and ran the custom command on the home item:

ran-et-revolver

As you can see the tokens were expanded:

home-item-expanded-tokens

You might be thinking “that’s wonderful Mike — except now I have to navigate to every item in my content tree using Revolver, and then run this custom command on it”.

Well, I do have a solution for this: a custom script that grabs an item and all of its descendants using a Sitecore query, and passes them to the custom command to expand tokens:

@echooff
@stoponerror

if ($1$ = \$1\$) (exit (Missing required parameter path))

@echoon

query -ns $1$/descendant-or-self::* et

I put this script in the core database, and named it “etr” for “Expand Tokens Recursively”:

etr-script

I navigated to a descendant of /sitecore/content/home, and see that it has some unexpanded tokens on it:

descendant-unexpanded-tokens

I went back to Revolver, and ran the “etr” command on the home item:

etr-revolver

As you can see tokens were expanded on the descendant item:

descendant-expanded-tokens

If you have any thoughts on this, or have ideas for other custom commands in Revolver, please share in a comment.

Shortcodes in Sitecore: A Proof of Concept

Today I stumbled upon a post in one of the SDN forums asking whether anyone had ever implemented shortcodes in Sitecore.

I have not seen an implementation of this for Sitecore — if you know of one, please drop a comment — but am quite familiar with these in WordPress — I use them to format code in my blog posts using the [code language=”csharp”]//code goes in here[/code] shortcode — and felt I should take on the challenge of implementing a “proof of concept” for this in Sitecore.

I first created a POCO that will hold shortcode data: the shortcode itself and the content (or markup) that the shortcode represents after being expanded:

namespace Sitecore.Sandbox.Pipelines.ExpandShortcodes
{
    public class Shortcode
    {
        public string Unexpanded { get; set; }

        public string Expanded { get; set; }
    }
}

I thought it would be best to put the logic that expands shortcodes into a new pipeline, and defined a pipeline arguments class for it:

using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.ExpandShortcodes
{
    public class ExpandShortcodesArgs : PipelineArgs
    {
        public string Content { get; set; }
    }
}

There really isn’t much to this arguments class — we will only be passing around a string of content that will contain shortcodes to be expanded.

Before moving forward on building pipeline processors for the new pipeline, I saw that I could leverage the template method pattern to help me process collections of Shortcode instances in an abstract base class:

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandShortcodes
{
    public abstract class ExpandShortcodesProcessor
    {
        public virtual void Process(ExpandShortcodesArgs args)
        {
            if (string.IsNullOrWhiteSpace(args.Content))
            {
                return;
            }

            IEnumerable<Shortcode> shortcodes = GetShortcodes(args.Content);
            if (shortcodes == null || !shortcodes.Any())
            {
                return;
            }

            args.Content = ExpandShortcodes(shortcodes, args.Content);
        }

        protected abstract IEnumerable<Shortcode> GetShortcodes(string content);

        protected virtual string ExpandShortcodes(IEnumerable<Shortcode> shortcodes, string content)
        {
            Assert.ArgumentNotNull(shortcodes, "shortcodes");
            Assert.ArgumentNotNull(content, "content");
            string contentExpanded = content;
            foreach (Shortcode shortcode in shortcodes)
            {
                contentExpanded = contentExpanded.Replace(shortcode.Unexpanded, shortcode.Expanded);
            }

            return contentExpanded;
        }
    }
}

The above class iterates over all Shortcode instances, and replaces shortcodes with their expanded content.

Each subclass processor of ExpandShortcodesProcessor are to “fill in the blanks” of the algorithm defined in the base class by implementing the GetShortcodes method only — this is where the heavy lifting of grabbing the shortcodes from the passed string of content, and the expansion of these shortcodes are done. Both are then set in new Shortcode instances.

Once the base class was built, I developed an example ExpandShortcodesProcessor subclass to expand [BigBlueText]content goes in here[/BigBlueText] shortcodes (in case you’re wondering, I completely fabricated this shortcode — it does not exist in the real world):

using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace Sitecore.Sandbox.Pipelines.ExpandShortcodes
{
    public class ExpandBigBlueTextShortcodes : ExpandShortcodesProcessor
    {
        protected override IEnumerable<Shortcode> GetShortcodes(string content)
        {
            if(string.IsNullOrWhiteSpace(content))
            {
                return new List<Shortcode>();
            }

            IList<Shortcode> shortcodes = new List<Shortcode>();
            MatchCollection matches = Regex.Matches(content, @"\[BigBlueText\](.*?)\[/BigBlueText\]", RegexOptions.IgnoreCase);

            foreach (Match match in matches)
            {
                string innerText = match.Groups[1].Value.Trim();
                if (!string.IsNullOrWhiteSpace(innerText))
                {
                    shortcodes.Add
                    (
                        new Shortcode
                        {
                            Unexpanded = match.Value,
                            Expanded = string.Format(@"<span style=""font-size:56px;color:blue;"">{0}</span>", innerText)
                        }
                    );
                }
            }

            return shortcodes;
        }
    }
}

I followed the above example processor with another — a new one to expand [YouTube id=”video id goes in here”] shortcodes (this one is made up as well, although YouTube shortcodes do exist out in the wild):

using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace Sitecore.Sandbox.Pipelines.ExpandShortcodes
{
    public class ExpandYouTubeShortcodes : ExpandShortcodesProcessor
    {
        protected override IEnumerable<Shortcode> GetShortcodes(string content)
        {
            if(string.IsNullOrWhiteSpace(content))
            {
                return new List<Shortcode>();
            }

            IList<Shortcode> shortcodes = new List<Shortcode>();
            MatchCollection matches = Regex.Matches(content, @"\[youtube\s+id=""(.*?)""\]", RegexOptions.IgnoreCase);

            foreach (Match match in matches)
            {
                string id = match.Groups[1].Value.Trim();
                if (!string.IsNullOrWhiteSpace(id))
                {
                    shortcodes.Add
                    (
                        new Shortcode
                        {
                            Unexpanded = match.Value,
                            Expanded = string.Format(@"", id)
                        }
                    );
                }
            }

            return shortcodes;
        }
    }
}

Next I built a renderField pipeline processor to invoke our new pipeline when the field is a text field of some sort — yes, all fields in Sitecore are fundamentally strings behind the scenes but I’m referring to Single-Line Text, Multi-Line Text, Rich Text, and the deprecated text fields — to expand our shortcodes:

using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Pipelines.RenderField;

using Sitecore.Sandbox.Pipelines.ExpandShortcodes;

namespace Sitecore.Sandbox.Pipelines.RenderField
{
    public class ExpandShortcodes
    {
        public void Process(RenderFieldArgs args)
        {
            if (!ShouldFieldBeProcessed(args))
            {
                return;
            }

            args.Result.FirstPart = GetExpandedShortcodes(args.Result.FirstPart);
        }

        private static bool ShouldFieldBeProcessed(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.FieldTypeKey, "args.FieldTypeKey");
            string fieldTypeKey = args.FieldTypeKey.ToLower();
            return fieldTypeKey == "text"
                    || fieldTypeKey == "rich text"
                    || fieldTypeKey == "single-line text"
                    || fieldTypeKey == "multi-line text";
        }

        private static string GetExpandedShortcodes(string content)
        {
            Assert.ArgumentNotNull(content, "content");
            ExpandShortcodesArgs args = new ExpandShortcodesArgs { Content = content };
            CorePipeline.Run("expandShortcodes", args);
            return args.Content;
        }
    }
}

I cemented all the pieces together using a Sitecore configuration file — this should go in your /App_Config/Include/ folder:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <expandShortcodes>
        <processor type="Sitecore.Sandbox.Pipelines.ExpandShortcodes.ExpandYouTubeShortcodes, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.ExpandShortcodes.ExpandBigBlueTextShortcodes, Sitecore.Sandbox" />
      </expandShortcodes>
      <renderField>
        <processor type="Sitecore.Sandbox.Pipelines.RenderField.ExpandShortcodes, Sitecore.Sandbox" 
                   patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetTextFieldValue, Sitecore.Kernel']" />
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Let’s see the above code in action.

I created a test item, and added BigBlueText and YouTube shortcodes into two different text fields:

shortcode-item-test

I saved, published, and then navigated to the test item:

shortcode-page-rendered

As you can see, our shortcodes were expanded.

If you have any thoughts on this, or ideas around a better shortcode framework for Sitecore, please share in a comment.