Category Archives: Pipeline

Handle URL redirects in a Sitecore multisite solution using FileMapProvider

redirect-me-baby_3

I would like to share with you guys how you could do URL redirects in a Sitecore multisite solution with Microsoft’s URL Rewrite Module using the FileMapProvider.

FilemapProvider is very cool, it reads the URL mappings from a text file.

It can be used instead of the built-in rewrite maps functionality when the amount of rewrite map entries is very large and it is not practical to keep them in a web.config file.

And the best thing of all:
No need to do any custom pipelines in Sitecore – it’s clean 🙂
You don’t have to do an IIS reset after the redirect file has been changed/updated.

The idea is this:
We have a Sitecore solution with several websites, each website will have a number of redirects. In Sitecore we will have a Bucket with “URL Redirects” items, each item will contain an old and a new url. From the Content Editor we want to be able to click on a command button that will trigger a remote event(we need it to work in a multi-server environment), which will write the redirects to a text file(for the FileMapProvider).

How about divide the work into following:
1. Configure the FileMapProvider/s.
2. Setup the URL redirects in Sitecore.
3. Create the command button in Sitecore Content Editor.
4. Create the (remote) event and write redirects to file.

1. Configure the FileMapProvider/s.

The FileMapProvider is part of the URL Rewrite Extensibility Samples, which allows us to use custom providers for the rewrite\redirect rules – in our case: external .txt file with URL’s.

/old/catalog/product; /new/category/product
/old/contactus/index; /new/contactus

In our multisite scenario we will have two websites running, testsite1.com and testsite2.com. Which means we will have two FileMapProvider’s, one for each website.

Let’s have a look at how the FilemapProviders are configured in the UrlRewrite.config. Yes it’s a web.config transformation(because Sitecore config file patching only works within the sitecore section)

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.webServer>
	<rewrite>
	  <providers>
		<provider name="FileMapProviderForTestSite1" type="FileMapProvider, Microsoft.Web.Iis.Rewrite.Providers, Version=7.1.761.0, Culture=neutral, PublicKeyToken=0545b0627da60a5f"  xdt:Transform="Insert">
		  <settings>
			<add key="FilePath" value="D:\Files\Data\UrlRedirectsForTestSite1.txt"/>
			<add key="IgnoreCase" value="1"/>
			<add key="Separator" value=";"/>
		  </settings>
		</provider>
		<provider name="FileMapProviderForTestSite2" type="FileMapProvider, Microsoft.Web.Iis.Rewrite.Providers, Version=7.1.761.0, Culture=neutral, PublicKeyToken=0545b0627da60a5f"  xdt:Transform="Insert">
		  <settings>
			<add key="FilePath" value="D:\Files\Data\UrlRedirectsForTestSite2.txt"/>
			<add key="IgnoreCase" value="1"/>
			<add key="Separator" value=";"/>
		  </settings>
		</provider>
	  </providers>
	  <rules>
		<rule name="RuleForFileMapProviderForTestSite1" stopProcessing="false" xdt:Transform="Insert">
		  <match url="(.*)"/>
		  <conditions>
		    <add input="{HTTP_HOST}" pattern="^testsite1.com$"/>
			<add input="{FileMapProviderForTestSite1:{REQUEST_URI}}" pattern="(.+)"/>
		  </conditions>
		  <action type="Redirect" url="{C:1}" appendQueryString="false"/>
		 </rule>
		 <rule name="RuleForFileMapProviderForTestSite2" stopProcessing="false" xdt:Transform="Insert">
		  <match url="(.*)"/>
		  <conditions>
		    <add input="{HTTP_HOST}" pattern="^testsite2.com$"/>
			<add input="{FileMapProviderForTestSite2:{REQUEST_URI}}" pattern="(.+)"/>
		  </conditions>
		  <action type="Redirect" url="{C:1}" appendQueryString="false"/>
		</rule>
	  </rules>
	</rewrite>
  </system.webServer>
</configuration>		

If you notice there are two FileMapProvider configurations, one provider for each site – how cool is that 🙂 The provider points to a text file(containing the redirects for that specific site).
Now in order to get the redirect magic to work we need to set up a rule. If you look at the rule RuleForFileMapProviderForTestSite1, you will see that this rule will only work for website – testsite1.com.
We do that by using pattern(below) to check if it matches with HTTP_HOST:

pattern="^testsite1.com$"

Then we use/get the “site” specific provider, FileMapProviderForTestSite1, to do the redirect.

Tip!
If you guys don’t want to install the URL Rewrite Extensibility Sample on a test or production server, fear not. Just grab it from your GAC on your local dev machine.
Fire up your PowerShell(for windows) and use the SUBST Command. Suppose you want to create a G Drive (G for GAC), use the following command:

SUBST G: C:\WINDOWS\ASSEMBLY

Look in folder G:\GAC_MSIL and you will find Microsoft.Web.Iis.Rewrite.Providers 🙂

2. Setup the URL redirects in Sitecore

Next will be to setup the URL redirects in sitecore. The folder structure for the redirects.
treeredirects

UrlRedirectsItem: It will contain two fields – NewUrl and OldUrl
redirectitem

UrlRedirectsFolderItem: I’ve added a field in order to figure out what site it belongs to.
folderitem

3. Create the command button in Sitecore Content Editor

Next will be to create the command that will trigger the writing of redirects to a text file. I wanted the button only to be visible when the “UrlRedirects folder” is selected/marked:
contenteditorcommand
I also added a confirm dialog, in case the editor wants to cancel the request.

In the Sitecore Core database we will add the command button, we will use the Large Button template. Navigate to /sitecore/content/Applications/Content Editor/Ribbons/Contextual Ribbons and create the new ribbon – Sync Url Redirects:
corecommand
We will also create a new command – SyncUrlRedirects:ToFile

In order for the command button only to be visible for a specific template(in this case the Url Redirects folder) we need to go to the template and then in the Appearance Section locate the drop tree field, Ribbon. And finally select our newly created ribbon – Sync Url Redirects:
templatecommand

We will patch the new command to a config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="SyncUrlRedirects:ToFile" type="Sandbox.Website.Commands.SyncUrlRedirectsFromSitecoreToFileCommand,Sandbox.Website" />
    </commands>
  </sitecore>
</configuration> 

Now we need to add some code for the command button.
In the Execute method we will verify that we are standing on the UrlRedirectsSiteFolder item, next will be to get all the children(deriving from template UrlRedirect). The GetChildrenByTemplateWithFallback method is an extension method from the Sitecore.ContentSearch.Utilities.
I need to send some parameters to the Confirm dialog(number of redirect items and the item id of the Redirects Folder) and finally call/execute the Confirm dialog by using Sitecore.Context.ClientPage.Start.

public class SyncUrlRedirectsFromSitecoreToFileCommand : Command
{

	private const string QueryParamRedirectsToBeSynced = "count";
	private const string QueryParamFolderId = "folderid";

	public override void Execute(CommandContext context)
	{
		Assert.ArgumentNotNull(context, "context");

		Assert.IsNotNull(context.Items, "context items are null");

		Assert.IsTrue(context.Items.Length > 0, "context items length is 0");

		Item item = context.Items[0];

		Assert.IsNotNull(item, "Item is null");

		Assert.IsTrue(item.IsDerived(SitecoreTemplates.UrlRedirectsSiteFolder.ID), "Item is not of template UrlRedirectsSiteFolder");

		int numberOfRedirects =
		item.GetChildrenByTemplateWithFallback(SitecoreTemplates.UrlRedirect.ID.ToString()).Count();

		context.Parameters.Add(QueryParamRedirectsToBeSynced, numberOfRedirects.ToString());
		context.Parameters.Add(QueryParamFolderId, item.ID.ToString());

		Sitecore.Context.ClientPage.Start(this, "Confirm", context.Parameters);
	}

	protected void Confirm(ClientPipelineArgs args)
	{

		if (args.IsPostBack)
		{
			switch (args.Result)
			{
				case "yes":
					SyncUrlRedirectsEvent syncUrlRedirectsEvent = new SyncUrlRedirectsEvent(args.Parameters[QueryParamFolderId]);
					Sitecore.Eventing.EventManager.QueueEvent(syncUrlRedirectsEvent, true, true);
					return;

				case "no":
					return;
			}
		}

		SheerResponse.Confirm($"It is important that you publish all redirects before you continue. {args.Parameters[QueryParamRedirectsToBeSynced]} redirects will be synced.");
		args.WaitForPostBack();

	}

	public override CommandState QueryState(CommandContext context)
	{
		Assert.ArgumentNotNull(context, "context");

		if (context.Items.Length != 1)
			return CommandState.Hidden;

		Item item = context.Items[0];

		Assert.IsNotNull(item, "First context item is null");

		Sitecore.Data.Version[] versionNumbers = item.Versions.GetVersionNumbers(false);
		if (versionNumbers == null || versionNumbers.Length == 0 || item.Appearance.ReadOnly)
			return CommandState.Disabled;

		if (!item.Access.CanWrite() || !item.Access.CanRemoveVersion() || IsLockedByOther(item))
			return CommandState.Disabled;

		return base.QueryState(context);

	}

}

In the Confirm method we will check if the if the editor hits the yes button, if so we will call the SyncUrlRedirectsEvent and pass along the item id of the Redirects Folder.

4. Create the (remote) event and write redirects to file

In order to write the redirects to a file on a CD server, we will need a remote event – SyncUrlRedirectsEvent. The event is quite simple, it will just hold the item id of the Redirects Folder.
Here is the event and its EventArgs class:

[DataContract]
public class SyncUrlRedirectsEvent
{
	public SyncUrlRedirectsEvent(string urlRedirectsSiteFolderItemId)
	{
		UrlRedirectsSiteFolderItemId = urlRedirectsSiteFolderItemId;
	}

	[DataMember]
	public string UrlRedirectsSiteFolderItemId { get; protected set; }

}

[Serializable]
public class SyncUrlRedirectsEventArgs : EventArgs, IPassNativeEventArgs
{
	public SyncUrlRedirectsEventArgs(string urlRedirectsSiteFolderItemId)
	{
		UrlRedirectsSiteFolderItemId = urlRedirectsSiteFolderItemId;
	}

	public string UrlRedirectsSiteFolderItemId { get; protected set; }
}

Next is the eventhandler, that is the one which will be called/executed on the CD server, so we can write the redirects for the FileMapProvider. Here we fetch all redirects from the “UrlRedirectsSiteFolderItem id”(which we got from the SyncUrlRedirectsEventArgs) and then write them to a text file.
I will not go into how the SyncUrlRedirectsService works, it’s quite straightforward. If you are interested in how it’s done just ping me and I will show you 🙂

public class SyncUrlRedirectsEventHandler
{
	private static ILogger _logger;
	private readonly ISyncUrlRedirectsService _syncUrlRedirectsService;
	private readonly IDatabaseRepository _databaseRepository;


	public SyncUrlRedirectsEventHandler()
	{
		_logger = IocContext.NonRequestContainer.GetInstance<ILogger>();
		_syncUrlRedirectsService = IocContext.NonRequestContainer.GetInstance<ISyncUrlRedirectsService>();
		_databaseRepository = new DatabaseRepository();
			
	}

	public virtual void OnRemoteSyncUrlRedirects(object sender, EventArgs e)
	{

		if (e == null)
		{
			_logger.Error("SyncUrlRedirectsEventArgs is empty");
			return;
		}
			

		SyncUrlRedirectsEventArgs syncUrlRedirectsEventArgs = (SyncUrlRedirectsEventArgs) e;

		if (syncUrlRedirectsEventArgs.UrlRedirectsSiteFolderItemId.IsNullOrWhiteSpace())
		{
			_logger.Error("UrlRedirectsSiteFolder item id is missing in SyncUrlRedirectsEventArgs");
			return;
		}

		Item urlRedirectsSiteFolderItem =
			_databaseRepository.ContextDatabase.GetItem(new ID(syncUrlRedirectsEventArgs.UrlRedirectsSiteFolderItemId));


		if (urlRedirectsSiteFolderItem == null)
		{
			_logger.Error("UrlRedirectsSiteFolderItem does not exist on {0}. Id: {1}", _databaseRepository.ContextDatabase.Name, syncUrlRedirectsEventArgs.UrlRedirectsSiteFolderItemId);
			return;
		}

		string sitename = urlRedirectsSiteFolderItem.GetString(SitecoreTemplates.UrlRedirectsSiteFolder.Fields.UrlRedirectsSiteName);

		if (sitename.IsNullOrWhiteSpace())
		{
			_logger.Error("Site name is not set in UrlRedirectsSiteFolder item id, {0}", urlRedirectsSiteFolderItem.ID);
			return;
		}


		IList<string[]> urlRedirectsList =	urlRedirectsSiteFolderItem.GetChildrenByTemplateWithFallback(SitecoreTemplates.UrlRedirect.ID.ToString())
			.Select(
				item =>
					new[] {item.GetString(SitecoreTemplates.UrlRedirect.Fields.UrlRedirectOldUrl), item.GetString(SitecoreTemplates.UrlRedirect.Fields.UrlRedirectNewUrl) }).ToList();


		Tuple<IList<string>, string> redirectsAndFilePath =  _syncUrlRedirectsService.PrepareRedirectsAndFilePathForFile(urlRedirectsList, sitename);

		_syncUrlRedirectsService.TryWriteRedirectsToFile(redirectsAndFilePath, sitename);

	}


	public static void Run(SyncUrlRedirectsEvent syncUrlRedirectsEvent)
	{
		_logger.Information("SyncUrlRedirectsEventHandler - Run", typeof(SyncUrlRedirectsEventHandler));

		SyncUrlRedirectsEventArgs args = new SyncUrlRedirectsEventArgs(syncUrlRedirectsEvent.UrlRedirectsSiteFolderItemId);

		Event.RaiseEvent("syncurlredirects:remote", args);
	}
}

The Run method is called from a Hook.

To glue it all together(event and eventhandler) we need to register and trigger the eventhandler, for that we will use a hook. See the hook as a very light pipeline.

public class SyncUrlRedirectsHook : IHook
{
	public void Initialize()
	{
		Sitecore.Eventing.EventManager.Subscribe(new Action<SyncUrlRedirectsEvent>(SyncUrlRedirectsEventHandler.Run));
	}
}

Here is the config file for the event and the hook:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
	<hooks>
	  <hook type="Sandbox.Website.Redirect.Events.SyncUrlRedirectsHook, Sandbox.Website" />
	</hooks>
    <events>
      <event name="syncurlredirects:remote">
        <handler type="Sandbox.Website.Redirect.Events.SyncUrlRedirectsEventHandler, Sandbox.Website" method="OnRemoteSyncUrlRedirects" />
      </event>
    </events>
  </sitecore>
</configuration>

Instead of using a hook, you can use a pipeline(for the event). Here are some great documentation regarding events in Sitecore:
http://sitecore-community.github.io/docs/pipelines-and-events/events/

That’s all for now folks 🙂

Advertisements

Query your datasource using custom tokens in Sitecore

Before heading into the post I would like to thank Sitecore for giving me the great honor of being awarded as one of the 167 MVPs. I truly enjoy sharing my experiences with people so this means a lot to me. Check out all the skilled MVPs at Sitecore MVPs 2015

Datasource

I would like to show you guys how to do queryable datasources for your renderings. This is nothing new and there are a lot of posts out there describing it very well, so why reinvent the wheel? I found some really nice posts and used most of their code.
Queries in Datasource location on Sitecore Layouts by Thomas Stern aka Dr Stern 🙂
Multiple Datasource Locations using Queries are back in Sitecore 7.2 Update-2 by Sean Holmesby

As Sean pointed out in his post – Since Sitecore 7.2 you can have multiple datasources, that is really cool.

Sitecore 7.2 Update-2 fixes this, and Sitecore’s default code will allow you to define multiple Datasource locations by having them pipe separated in the ‘Datasource Location’ field on a rendering/sublayout

I checked Sean’s and Thomas code and created my pipeline for supporting queryable datasources to the renderings. I started out doing some queries and it worked great but then I needed to to go “above” my site node to find its datasource folder.

Sitecore
–Content
—Site1
—Site2
—Site3
—Site4
—Datasource content
—–Global
—–Site1
——-Some folder
—–Site2
—–Site3
—–Site4

In other words I need to get the name of an element and then add it to the datasource path, something like this:

query:./ancestor-or-self::*[@@templatename='Site']/../Datasource content/*[@@name=name of site element])/Some folder

I read all kinds of stuff on what you can do with the query using XPath functions. Here are some good posts you should read:
SITECORE QUERY CHEAT SHEET
Querying Items from Sitecore

Good old Sitecore SDN stuff:
Sitecore Query Syntax
Data Definition Reference – chapter 4:3
Using Sitecore Fast Query

At the end I came to the conclusion that I needed following XPath function:
name(node) – XPath, XQuery, and XSLT Functions, unfortunately this one is not supported in Sitecore’s XPath

So what to do? Why not make a custom token since I already have my own pipeline for the queryable datasource. The custom token should be something like this:

<< node?Field attribute >> 
<<ancestor-or-self::*[@@templatename='Site']?@name>>   

I found this nice old post about accessing special properties of a Sitecore item the same way you get a typical field’s value:
Getting a special field value in Sitecore by Sean Kearney

Here is how the datasource finally will look like:

/sitecore/content/Datasource content/Global/Social sharing|query:./ancestor-or-self::*[@@templatename='Site']/../Datasource content/*[@@name=<<ancestor-or-self::*[@@templatename='Site']?@name>>])/Social sharing

Here is the code for the pipeline:

public class MultipleDataSourceLocationsWithQueries
{
    private IDatasourceQueryService _datasourceQueryService; 

    public void Process(GetRenderingDatasourceArgs args)
    {
        Assert.IsNotNull(args, "args");

        this._datasourceQueryService = new DatasourceQueryService(args.ContextItemPath, args.ContentDatabase, args.RenderingItem["Datasource Location"], args.DatasourceRoots);

        if (!this._datasourceQueryService.IsQueryInDataSourceLocation()) 
            return;

        this._datasourceQueryService.ProcessQuerys();
    }
}

Then I created a service for the datasource stuff.

public class DatasourceQueryService : IDatasourceQueryService
{
    private readonly string _contextItemPath;
    private readonly Database _contentDatabase;
    private readonly string _datasourceLocation;
    private readonly List<Item> _datasourceRoots;
    private readonly Regex _regEx;

    public DatasourceQueryService(string contextItemPath, Database contentDatabase, string datasourceLocation, List<Item> datasourceRoots)
    {
        this._contextItemPath = contextItemPath;
        this._contentDatabase = contentDatabase;
        this._datasourceLocation = datasourceLocation;
        this._datasourceRoots = datasourceRoots;
        this._regEx = new Regex(GetRegexPattern());
    }

        
    public bool IsQueryInDataSourceLocation()
    {
        return this._datasourceLocation.Contains(QueryIdentifier);
    }

    public void ProcessQuerys()
    {
        ListString possibleQueries = new ListString(this._datasourceLocation);
        foreach (string possibleQuery in possibleQueries)
        {
            if (possibleQuery.StartsWith(QueryIdentifier))
            {
                ProcessQuery(possibleQuery);
            }
        }
    }

    private string GetRegexPattern()
    {
        return String.Format(".*{0}(.*){1}.*", SpecialStartItemDelimiter, SpecialEndItemDelimiter);
    }


    private void ProcessQuery(string query)
    {
        //Remove the "query:."
        query = query.Replace(QueryIdentifier, "");

        Item[] datasourceLocations = ResolveDatasourceRootFromQuery(query);

        if (datasourceLocations == null || !datasourceLocations.Any()) 
            return;

        foreach (Item dataSourceLocation in datasourceLocations.Where(dataSourceLocation => !this._datasourceRoots.Exists(item => item.ID.Equals(dataSourceLocation.ID))))
        {
            this._datasourceRoots.Add(dataSourceLocation);
        }
    }

    private Item[] ResolveDatasourceRootFromQuery(string queryPath)
    {
        Match matchedData = this._regEx.Match(queryPath);

        int numberOfMatchedTokens = 1;

        while (matchedData.Success)
        {
            //Just in case - I hate while loops
            if (numberOfMatchedTokens > MaxNumberOfIterations)
                break;
                
            queryPath = GenerateQueryPathForSpecialToken(matchedData, queryPath);
            matchedData = this._regEx.Match(queryPath);

            numberOfMatchedTokens++;
        }

        return GetItemsFromQuery(queryPath);
    }

    private string GenerateQueryPathForSpecialToken(Match matchedData, String queryPath)
    {
        string specialQueryPath = matchedData.Groups[1].ToString();

        string translatedValue = ResolveSpecialItemInQuery(specialQueryPath);

        return queryPath.Replace(
            string.Format("{0}{2}{1}", SpecialStartItemDelimiter, SpecialEndItemDelimiter, specialQueryPath),
            translatedValue);
    }

    private Item[] GetItemsFromQuery(string queryPath)
    {
        try
        {
            return this._contentDatabase.SelectItems(string.Format("{0}/{1}", this._contextItemPath, queryPath));
        }
        catch (Exception ex)
        {
            Log.Error(String.Format(@"Datasource query was not valid:{0}", queryPath), ex, this);
            return null;
        }
    }

    private string ResolveSpecialItemInQuery(string specialQueryPath)
    {
        if (!specialQueryPath.Contains(FieldDelimiter))
            return string.Empty;

        string specialItemPath = specialQueryPath.Split(char.Parse(FieldDelimiter))[0];

        if (string.IsNullOrWhiteSpace(specialItemPath))
            return string.Empty;

        Item[] specialItems = GetItemsFromQuery(specialItemPath);

        if (!specialItems.Any())
            return string.Empty;

        string fieldProperty = specialQueryPath.Split(char.Parse(FieldDelimiter))[1];

        if (string.IsNullOrWhiteSpace(fieldProperty))
            return string.Empty;

        return specialItems.Select(item => item[fieldProperty]).FirstOrDefault();

    }

    private const string QueryIdentifier = "query:.";
    private const string SpecialStartItemDelimiter = "<<";
    private const string SpecialEndItemDelimiter = ">>";
    private const string FieldDelimiter = "?";
    private const Int32 MaxNumberOfIterations = 10;

}

The interface for the service

public interface IDatasourceQueryService
{
    bool IsQueryInDataSourceLocation();

    void ProcessQuerys();
}

In order to make the pipeline work we need to do a patch config file.

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getRenderingDatasource>
       <processor type="Sandbox.QueryableDatasource.Pipelines.GetRenderingDatasource.MultipleDataSourceLocationsWithQueries, Sandbox.QueryableDatasource"
                   patch:after="processor[@type='Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel']" />
        </processor>
      </getRenderingDatasource>
    </pipelines>
  </sitecore>
</configuration>

That’s all for now folks 🙂