Utilizing the ItemPathResolver in Sitecore

Utilizing the ItemPathResolver in Sitecore

Recently, I ran into a situation where I needed to validate an item exists before proceeding to a custom page, by being given an arbitrary path. The page displays information from an API that also will somewhat rely on this path as well, but we do not want end users to attempt to land on a page that does not exist. We won’t be focusing on the creation of this API or page itself, purely going to be in the context of a RequestProcessor.

To better show this example, consider the below URL as the assumed template, and one passed in by an end user.

www.{hostSite}.com/{location}/{product}/{type}/available
www.{hostSite}.com/orlando-location/products/books/available

Another key to this is knowing how the client names their items, let’s assume the content tree looks like so

.
└── Sitecore/
    └── Content/
        └── Home/
            ├── Available
            ├── Orlando-Location/
            │   └── products/
            │       └── books/
            │           └── ...
            ├── Fort Worth Location/
            │   └── products/
            │       └── books/
            │           └── ...
            └── Online Only/
                └── products/
                    └── books/
                        └── ...

The ‘Available’ page is landing page, this will be the page rendered when you hit a location like above, and will display the information dynamically.

Paths are set to use dashes by default, but with display name functionality, some content editors have not followed this path.

The path to available product has to work whether an end user does dashes or spaces in the URL, as there is a mix.

To help me check some boxes at the end of the task, my 3 main tasks are like so:

TaskAcceptance Criteria
dashes and spaces both return the same page/resultspage will render as long as the path is real
only runs on the location itemswill re-route user if not location item templated item
no injection into the URLinputs are sanitized

So how do you begin a task like this? Well, in order to do this, we have to override the HttpRequestProcessor pipeline to fit this specific need. With the above information as our baseline, we already know what sort of checks we will have in this processor.

Let’s set up a really basic one, and then iterate a little on top of that. Here is a very barebones override, simply checking if the last request segment is

...
namespace Client.Web.Pipelines
{
	public class AvailableProductHttpProcessor : HttpRequestProcessor
	{
		/// <summary>
		/// Required override for processor
		/// </summary>
		/// <param name="args"></param>
		public override void Process(HttpRequestArgs args)
		{
			Assert.ArgumentNotNull(args, "args");

			if (Context.Item != null
				|| Context.Database == null
				|| Context.Site.Name != "website"
				|| args.Url.ItemPath.Length == 0
				|| args.HttpContext.Request.Url.Segments.Last().ToLower() != "available")
			{
				return;
			}
	}
}
...

So here, we have a few checks here these are nothing crazy but we will do a quick overview just to help illustrate:

  • Make sure the request is essentially not null are not null
  • We then verify some things in an if statement
    • context checks to make sure we don’t break on a valid item
    • break out of the override if the last segment is not available

Now we need it to do something, and this is where we get into the task at hand.

I can only assume so much about the input, so I need a way to validate the URL matches a valid path, otherwise this needs to go to a 404 or preferably get redirected.

Let’s start by breaking apart the request path into a list that we can work with in the ItemPathResolver. This can be very easily achieved with a simple query to the url segments like so:

			// validate the path is valid for product availability request
			var segments = args.HttpContext.Request.Url.Segments.AsQueryable().Where(i => i.Equals("available", StringComparison.OrdinalIgnoreCase) != true)
																				.Where(i => i.Equals("/") != true)
																				.ToList();

To quickly break this down to help understand, you can see I set the URL segments from the args to ‘AsQueryable’, filter out ‘available’ using the OrdinalIgnoreCase to catch any case scenario. I also filter out empty slashes, which will always be the first in the URL segments.

assuming we gave the URL from above (www.{hostSite}.com/orlando-location/products/books/available) for this particular request, you would get the following:

{"/orlando-location",
"/products",
"/books"}

Now that we have each segment, we want to validate each one to make sure the item exists so that we can properly show the data on the landing page.

For this, I built out a private PathValidator method that I give this list of strings, it does some magic and then it returns true or false. Okay, that’s all, thanks for coming.

JK. Let’s dive into the ‘magic’ of it, as this is the part I am most excited about.

a little fun background on the situation and why this one was fun for me, was that I did not have an easy way initially to do this. The original implementation that I saw had so much unmaintainable hyper specific regex for filtering out dashes, triple dashes, weird spacing, basically anything you could thing of, nested in billions and billions of if statements. I just could not look at it. It also, did not meet my needs well enough, so I had to start over. I tried to understand how Sitecore itself natively managed to resolve items, since that always worked regardless of spaces/dashes/etc – but I could not find the specific, or reusable bits of code or references. So I started over again, and dug through the Sitecore DLL’s, particularly the references around items and data.

This was incredibly helpful, as this showed me a few ItemPathResolver methods I could use, and in my mind, this was what I was looking for! However, the documentation on the true functionality was severely lacking. The core things, were it took 2 parameters, but this was easily found by just looking at the method. The 2 params were the root item to search under, and the second was the child item you wanted to find. I had to find out anything else on my own. I did this by experimenting some, utilizing the debugger, and just simply looking at the parameters passed into it, I was able to utilize it to the fullest. This was huge, as it did almost exactly what Sitecore does OOB, so I was happy to use it. It definitely is part of my toolbox now.

The bool I built utilizes an instance of Sitecore.Data.ItemResolvers.ItemPathResolver(), taking 2 params, a string path to the item, and an Item object root to search underneath. (example: Home Item)

Here is the method in its entirety, but I will break it down to make more sense below.

		/// <summary>
		/// Validate path and return true or false based on string list
		/// </summary>
		/// <param name="path"></param>
		/// <returns>true if item resolved, false if null</returns>
		private static bool ValidatePath (List<string> path)
		{
			// Init itempathresolver instance
			var pathResolver = new Sitecore.Data.ItemResolvers.ItemPathResolver();

			// Set the root node for the search (home node at first, since we do not know path currently exists)
			var homePath = Context.Site.StartPath;
			var rootItem = Context.Site.Database.GetItem(homePath);

			// Foreach Segment in path, validate it
			foreach (var seg in path)
			{
				// Clean the segment, URL decode it, you could also add some other checks if you want.
				// Remove any slashes from the segment we are working with, use the site configured name encoding to encode it
				// Set a default resolvedSeg to null, this is what we pass back if all segments return true/false
				var cleanSeg = HttpUtility.UrlDecode(seg);
				var encSeg = MainUtil.EncodeName(cleanSeg.Replace("/", string.Empty));
				Item resolvedSeg = null;

				// run it. Validate a segment based on the last valid segment (that was then made the 'rootItem')
				if (rootItem != null)
                {
					resolvedSeg = pathResolver.ResolveItem(encSeg, rootItem);

				}

				// set root node and keep pushing. 
				if (resolvedSeg != null)
				{
					// set the root for the next run.
					rootItem = resolvedSeg;
				}
				else
				{
					// May not ever happen, but just in case. if the rootItem is not null, try again.
					// There is still a template validation below.
					if (rootItem != null)
					{
						// try one more time
						// for this, do a comparison, check the display name if one exists and the main name, encode to do a compare
						rootItem = rootItem.Children?
							.Where(i => string.Compare(MainUtil.EncodeName(i.DisplayName ?? i.Name), encSeg, true) == 0)
								.FirstOrDefault();
					}
					else
					{
						// in the unlikely event, null rootItem and break the cycle.
						rootItem = null;
						break;
					}
				}
			}

			// if true, validate the template is one we support before returning response.
			// You could call a constants file here to name your templates, I just chose this route for the example
			bool valid = rootItem != null && (rootItem.TemplateName.Equals("location landing", StringComparison.OrdinalIgnoreCase)
												|| rootItem.TemplateName.Equals("products landing", StringComparison.OrdinalIgnoreCase)
												|| rootItem.TemplateName.Equals("book Item", StringComparison.OrdinalIgnoreCase)
												|| rootItem.TemplateName.Equals("customProduct Item landing", StringComparison.OrdinalIgnoreCase));

			return valid;
		}

So I have heavily commented the file (for my sake and yours), but I would like to break this down into digestible parts as bullet points.

To start, the method itself is simply a bool, since this is taking place as a request override. I just need to simply know if the path I am feeding it, has content items that are valid and match the templates I am looking for.

private static bool ValidatePath (List<string> path)

I then set up a new instance of the the ItemPathResolver(), as this is the main thing we will use to check an item. It does a lot of the heavy lifting, I just have to do my part. In theory, you could pass the home content Item object, and the full path you are looking for, but I have to assume that since this is reliant on user input, that that input could be wrong, malicious or old just plain invalid. So to validate all segments input, I set the 2 constants up, that I will update as I go.

			// Init itempathresolver instance
			var pathResolver = new Sitecore.Data.ItemResolvers.ItemPathResolver();

I set the homePath as the site Context.Site.StartPath – this should dynamically give you a string to your website home (ex: ‘sitecore/content/home’) – this will help get the root item for the context website. You could hardcode this value if you want, but I wanted flexibility and reusability, and this felt the best way to do it. I then use this path to get the actual root item for use later.

			// Set the root node for the search (home node at first, since we do not know path currently exists)
			var homePath = Context.Site.StartPath;
			var rootItem = Context.Site.Database.GetItem(homePath);

Pass in the segment list into a foreach where I utilize httpUtility to decode the segment if needed, then I pass that individual segment into a name encoded variable, this uses the site configured name encoding to encode that segment. I also set a null default Item variable for resolvedSeg that will be updated if a valid item is found.

			foreach (var seg in path)
			{
				var cleanSeg = HttpUtility.UrlDecode(seg);
				var encSeg = MainUtil.EncodeName(cleanSeg.Replace("/", string.Empty));
				Item resolvedSeg = null;
			...
			}

I do a quick check of the rootItem to make sure that is not null for whatever reason, and then I attempt to resolve that segment by running ResolveItem with my 2 params.

				// run it. Validate a segment based on the last valid segment (that was then made the 'rootItem')
				if (rootItem != null)
                {
					resolvedSeg = pathResolver.ResolveItem(encSeg, rootItem);

				}

Check if the resolved segment is not null – which it will be if not found – and then set the rootItem to this for the next loop.

			// set root node and keep pushing. 
				if (resolvedSeg != null)
				{
					// set the root for the next run.
					rootItem = resolvedSeg;
				}
				else
				{
				    ...
				}

Should the resolved segment be null, but the rootItem be valid, we will do one more fallback search just in case, and we will just do a query against the child items of the rootItem for the name of the segment. This is not that likely to happen, but just in case, we want to look for displayname or item name itself directly. Again, in theory, it should not happen, but in the rare chance it does, this should be there to catch it.

				else
				{
					// May not ever happen, but just in case. if the rootItem is not null, try again.
					// There is still a template validation below.
					if (rootItem != null)
					{
						// try one more time
						// for this, do a comparison, check the display name if one exists and the main name, encode to do a compare
						rootItem = rootItem.Children?
							.Where(i => string.Compare(MainUtil.EncodeName(i.DisplayName ?? i.Name), encSeg, true) == 0)
								.FirstOrDefault();
					}
					else
					{
						// in the unlikely event, null rootItem and break the cycle.
						rootItem = null;
						break;
					}
				}

finally, we break out of the loop and then validate if the item finally passed back is not null, and a valid template for our rendering needs.

			// if true, validate the template is one we support before returning response.
			// You could call a constants file here to name your templates, I just chose this route for the example
			bool valid = rootItem != null && (rootItem.TemplateName.Equals("location landing", StringComparison.OrdinalIgnoreCase)
												|| rootItem.TemplateName.Equals("products landing", StringComparison.OrdinalIgnoreCase)
												|| rootItem.TemplateName.Equals("book Item", StringComparison.OrdinalIgnoreCase)
												|| rootItem.TemplateName.Equals("customProduct Item landing", StringComparison.OrdinalIgnoreCase));

			return valid;

That is all there is to the validate path! Now, to implement it into the request processor, it is very easy. adding to the code from before, we can simply set this to a bool variable, and make a simple decision to proceed or end that request.

	public class AvailableProductHttpProcessor : HttpRequestProcessor
	{
.       ...
            // We do a little validating
			bool validPath = ValidatePath(segments);

			if (validPath == true)
			{
				// success, show products
				Context.Item = Context.Database.GetItem("/sitecore/content/Home/Available");
			}
			else
			{
				// redirect
				return;
			}
       ...
   }

To conclude this, it should not work with valid, legitimate URLs and should not work on a URL where it should not (ex: www.{HOSTSITE}.com/about/available) and it should not care about dashes or spaces in the URL, it should resolve both.

Of course, you can do this an infinite number of ways I am sure, this is just the one I found fit my needs, was maintainable without any need to go update any of this code should structure change. Of course, the name ‘Available’ – could be placed into a constants file.

Thank you for reading, I hope this was helpful in showcasing the use of ItemPathResolver.

Leave a comment

Damien Rincon

From Debug to Deploy: Lessons from the Fullstack Trenches.