Thursday, October 30, 2014

Vulnerability with using the Sitecore context search index

Mornings when production issues happen mean two things: no tea yet, and definitely no snacks until order is restored. So I was in a bit of a hurry today to find out what was going on when our default Sitecore content indexes seemed to have gone down. This was the exception thrown:

System.NullReferenceException: Object reference not set to an instance of an object. at Sitecore.ContentSearch.SitecoreItemCrawler.IsAncestorOf(Item item) at Sitecore.ContentSearch.SitecoreItemCrawler.IsExcludedFromIndex(SitecoreIndexableItem indexable, Boolean checkLocation) at Sitecore.ContentSearch.Pipelines.GetContextIndex.FetchIndex...

Here's a snippet that would cause the above error to be thrown:
Item contextItem = Sitecore.Context.Database.GetItem(SOME_ID);

if (contextItem != null)
{
    ISearchIndex index = ContentSearchManager.GetIndex(new SitecoreIndexableItem(contextItem));
}

That's just an example, but I would also see the error thrown in the __semantics field in my 'web' database.

So we're failing to retrieve the ISearchIndex... but why?

If you take a look at IsAncestorOf(Item item) (Sitecore 7.2+) using your favorite reflection tool, this is what you'll find:
// Sitecore.ContentSearch.SitecoreItemCrawler
protected virtual bool IsAncestorOf(Item item)
{
 bool result;
 using (new SecurityDisabler())
 {
  using (new CachesDisabler())
  {
   result = this.RootItem.Axes.IsAncestorOf(item);
  }
 }
 return result;
}
No null checks here, that can't be good.
The RootItem property -
// Sitecore.ContentSearch.SitecoreItemCrawler
public Item RootItem
{
    get
    {
        if (this.rootItem == null)
        {
            Database database = ContentSearchManager.Locator.GetInstance<IFactory>().GetDatabase(this.database);

            Assert.IsNotNull(database, "Database " + this.database + " does not exist");

            using (new SecurityDisabler())
            {
                this.rootItem = database.GetItem(this.Root);
            }
        }
        return this.rootItem;
    }
}
No null check here either. Fishy.

I had to dig a bit deeper to figure out where it all originates and what exactly is null.

I overwrote the Sitecore.ContentSearch.Pipelines.GetContextIndex.FetchIndex, Sitecore.ContentSearch pipeline processor to debug the code. It blew up trying to evaluate this:
   System.Collections.Generic.IEnumerable<ISearchIndex> enumerable =
                from searchIndex in ContentSearchManager.Indexes
                from providerCrawler in searchIndex.Crawlers
                where !providerCrawler.IsExcludedFromIndex(indexable)
                select searchIndex;
So the GetContextIndex pipeline tries to fetch all of the available crawlers, whose roots are ancestors of our indexable item (IsExcludedFromIndex). The ancestor check however fails on a null root item in any of the indexes.

So if you remember the good old "Root Item Not defined" error message,  it turns out this issue had the exact same cause - the RootItem for one of the search indexes was not defined in the 'web' database (i.e. the item was not yet published). Publishing is of course the quick fix.

While this is in the end a configuration issue, I still feel like the unpublished site should not be affecting ALL OF SEARCH (search based on GetContextIndex that is), which is why I'm going to leave my FetchIndex override in place and add some null checks for at least a more meaningful error message.

Tuesday, June 3, 2014

Custom rendering conditions for Sitecore presentation components

Conditional rendering rules in Sitecore are used to personalize specific components on a page based on visitor variables. It is fairly easy to extend the Rules Engine and create custom conditions to satisfy specific business needs and requirements. There are a lot of posts out there on how to create custom rules and conditions in Sitecore, so I'm going to try and keep this concise. There are three steps, The WHAT, The HOW, then Tying it all together. Enjoy!

1. The WHAT
The first step to creating a custom condition is defining what exactly it would do and choosing which base Sitecore condition type to inherit from. The below chart illustrates the inheritance between the base condition types in the Sitecore.Rules.Conditions namespace:



In this example, we are going to allow the user to select a specific value that represents a Sitecore item on the website UI, and personalize the content we render based on that value. Since this would be similar to the ItemIdCondition, which inherits from StringOperatorCondition, we will go with the same base.

2. The HOW
The below diagram illustrates the basic logic flow between our website (the almighty UI), our Sitecore rules engine component (the custom condition) and a custom value provider (a.k.a business domain code).

This separation of concerns allows us to contain all logic regarding the storage and retrieval of our value in its dedicated ValueProvider. Whether this value comes from a database, is stored in a cookie, or is a session variable is thus irrelevant to the condition and the evaluation of the condition. It also becomes irrelevant to the website UI, which will only delegate the responsibility of getting and setting the value to the same provider as well.

Here's an example of a condition class that uses a provider to retrieve the current context value. In an ideal scenario, instead of instantiated here, the provider implementation would be injected through a dependency injection container (pick your weapon carefully).

    public class SelectedValueCondition<T> : StringOperatorCondition<T> where T : RuleContext
    {
        private static ISomeValueProvider _someValueProvider;
        public static ISomeValueProvider MyAwesomeProviderInstance
        {
            get
            {
                if (_someValueProvider == null)
                {
                    _someValueProvider = new AwesomeProviderImplementation();
                }

                return _someValueProvider;
            }
        }

        private ID _itemid;
        public ID ItemID
        {
            get
            {
                return _itemid;
            }
            set
            {
                Assert.ArgumentNotNull(value, "item id");
                _itemid = value;
            }
        }

        public SelectedValueCondition()
        {
            _itemid = ID.Null;
        }

        public SelectedValueCondition(ID itemId)
        {
            Assert.ArgumentNotNull(itemId, "item id");
            _itemid = itemId;
        }
 
        protected override bool Execute(T ruleContext)
        {
            Assert.ArgumentNotNull(ruleContext, "ruleContext");

            // let's say our ValueModel class represents a domain entity that has a property named 'ID'
            ValueModel model = MyAwesomeProviderInstance.GetValue();

            ID modelId = !string.IsNullOrEmpty(model.ID) ? new ID(model.ID) : ID.Null;
            ID itemId = ItemID;

            return !modelId.IsNull && !itemId.IsNull && Compare(modelId.ToString(), _itemid.ToString());
        }
    }

3. Tying it all together
Adding our condition to Sitecore is straight-forward. An important thing to remember is to set the path of the items, from which a content editor should be able to choose from:

And that is it! Now this rule is ready to be applied to any presentation component that uses a data source.

Friday, May 30, 2014

Building support for multilingual content labels in Sitecore 7

When developing a multilingual site, one of the considerations (although maybe not one of the major ones) is translating all of those labels on buttons and other static elements on the page.

There are a couple of ways you could deal with those:
1.     Create a resource file to store string translations
2.     Use the built-in Sitecore dictionary: ex. Sitecore.Globalization.Translate.Text("More") 

From the two above, I would prefer to leave all translation responsibilities to the content editors (and possibly translation services...). However, the Sitecore Dictionary is hidden under /sitecore/system, which is not always available to content editors due to various permissions and access rights.


I wanted a way to allow the content editors to use a translation layer without having to alter permissions. So I created a Dictionary under /sitecore/content that would be accessible to content editors to create items and language versions. Since I didn't care much about the content hierarchy within the dictionary and I wanted quick and efficient word lookups, I marked the DictionaryItem template as bucketable and made my Dictionary a bucket.














I use the name of the dictionary item as a key for my look up and run a query to retrieve the first available search result with that name. Then all that's left is to get the corresponding item in the language version that's requested and return the text value.

This will work when you don’t want to grant your editors access to /sitecore/system. Content editors enter text into the item and create different language versions, then the static elements of the page get updated in accordance with the language version being requested. I haven’t fully tested the performance implications of doing this vs. the built-in Sitecore Dictionary, but I don’t foresee there being any large impact. If you have any suggestions, I’d love to hear them!


Below is my Dictionary class:

    /// <summary>
    /// The Dictionary class
    /// </summary>
    public class Dictionary
    {
        private const string DICTIONARY_PATH = "/sitecore/content/Data/Dictionary";
        private readonly ID DictionaryItemTemplateId = new ID("{C406784E-4E98-4671-ACC9-DCCFDF680B44}");

        /// <summary>
        /// Gets the dictionary value for the current language.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns></returns>
        public string GetDictionaryValue(string key)
        {
            return GetDictionaryValue(key, Sitecore.Context.Item.Language);
        }

        /// <summary>
        /// Gets the dictionary value.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="language">The language.</param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentNullException"></exception>
        public string GetDictionaryValue(string key, Language language)
        {
            string dictionaryValue;

            // key is a required paramater
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            if (language != null)
            {
                Item dictionary = Sitecore.Context.Database.GetItem(DICTIONARY_PATH);

                if (dictionary != null)
                {
                    ISearchIndex index = ContentSearchManager.GetIndex(new SitecoreIndexableItem(dictionary));
                    using (IProviderSearchContext searchContext = index.CreateSearchContext())
                    {
                        SearchResultItem searchResult = searchContext.GetQueryable<SearchResultItem>()
                            .Where(resultItem => resultItem.Path.Contains(dictionary.Paths.FullPath))
                            .Where(resultItem => resultItem.TemplateId == DictionaryItemTemplateId)
                            .FirstOrDefault(resultItem => resultItem.Name == key);

                        if (searchResult != null)
                        {
                            Item dictionaryItem = Sitecore.Context.Database.GetItem(searchResult.ItemId, language);
                            dictionaryValue = dictionaryItem["Text"];
                        }
                        else
                        {
                            //no translation found
                            dictionaryValue = key;
                        }
                    }
                }
                else
                {
                    // dictionary does not exist in the current context
                    Log.Error(string.Format("Dictionary Folder Item not found: {0}.", DICTIONARY_PATH), typeof (Dictionary));
                    dictionaryValue = key;
                }
            }
            else
            {
                // dictionary value cannot be translated without a specified language
                dictionaryValue = key;
            }

            return dictionaryValue;
        }
    }