Monday, September 28, 2015

Sitecore Web Forms for Marketers as a Service

Customizing the WFFM module for Sitecore has provided post material for quite a few blogs, including this one. It's a favorite among clients, and it's fairly extensible. However, we all know that once in a while, we get the occasional request for a marketing form, which the module simply does not work for. My last endeavor into forms included a fancy animated multiple step wizard with interdependent fields, which would be an immense undertaking to implement with WFFM versus a simple custom .net form.

So the question became, how do I combine the value we get out of WFFM setup, save actions and reporting with the amazing designs and form flowcharts, which the design teams come up with and which our front end developers code into clean and beautiful html5. Especially in cases where we've already developed quite a few save actions that post to various client CRMs, create leads, and talk to third parties. Who wants to recode all that for a single custom form?

So I started working towards a concept, which would provide an endpoint for WFFM form submissions. From anywhere. It is fairly simple and straight forward to implement (wait till we get to the code part), but it becomes powerful in that it allows developers to have full control over the rendered html and still take advantage of the WFFM save actions.

Step 1. Run WFFM save actions on submitted form data.

We'll need to define a couple of classes for consumers of the WFFM service

   public class FormData
    {
        public string FormId { get; set; }

        public IEnumerable<FormField> Fields { get; set; } 
    }
    public class FormField
    {
        public string FieldName { get; set; }

        public string FieldValue { get; set; }
    }
And the meaty part - the FormProcessor, which is responsible for running the WFFM save actions, and of course has a dependency on the Sitecore and WFFM assemblies. With the help of a reflection tool, we can imitate what WFFM does behind the scenes here:

    public class FormProcessor
    {
        public FormProcessorResult Process(FormData data)
        {
            FormProcessorResult result = new FormProcessorResult();

            if (string.IsNullOrEmpty(data.FormId))
            {
                result.Success = false;
                result.ResultMessage = "Invalid Form Id";
            }
            else
            {
                bool failed = false;

                ID formId = new ID(data.FormId);
                FormItem formItem = FormItem.GetForm(formId);

                if (formItem != null)
                {
                    //Get form fields of the WFFM
                    FieldItem[] formFields = formItem.FieldItems;

                    //Create collection of fields
                    List<AdaptedControlResult> adaptedFields = new List<AdaptedControlResult>();
                    foreach (FormField field in data.Fields)
                    {
                        FieldItem formFieldItem = formFields.FirstOrDefault(x => x.Name == field.FieldName);
                        if (formFieldItem != null)
                        {
                            adaptedFields.Add(GetControlResult(field.FieldValue, formFieldItem));
                        }
                        else
                        {
                            // log and bail out
                            Log.Warn(string.Format("Field Item {0} not found for form with ID {1}", field.FieldName, data.FormId), this);
                            failed = true;
                            result.Success = false;
                            result.ResultMessage = string.Format("Invalid field name: {0}", field.FieldName);
                            break;
                        }
                    }

                    if (!failed)
                    {
                        // Get form action definitions
                        List<ActionDefinition> actionDefinitions = new List<ActionDefinition>();
                        ListDefinition definition = formItem.ActionsDefinition;
                        if (definition.Groups.Count > 0 && definition.Groups[0].ListItems.Count > 0)
                        {
                            foreach (GroupDefinition group in definition.Groups)
                            {
                                foreach (ListItemDefinition item in group.ListItems)
                                {
                                    actionDefinitions.Add(new ActionDefinition(item.ItemID, item.Parameters)
                                                              {
                                                                  UniqueKey = item.Unicid
                                                              });
                                }
                            }
                        }

                        //Execute form actions
                        foreach (ActionDefinition actionDefinition in actionDefinitions)
                        {
                            try
                            {
                                ActionItem action = ActionItem.GetAction(actionDefinition.ActionID);
                                if (action != null)
                                {
                                    if (action.ActionType == ActionType.Save)
                                    {
                                        object saveAction = ReflectionUtil.CreateObject(action.Assembly, action.Class,
                                                                                        new object[0]);
                                        ReflectionUtils.SetXmlProperties(saveAction, actionDefinition.Paramaters, true);
                                        ReflectionUtils.SetXmlProperties(saveAction, action.GlobalParameters, true);
                                        if (saveAction is ISaveAction)
                                        {
                                            ((ISaveAction) saveAction).Execute(formId, adaptedFields, null);
                                        }
                                    }
                                }
                            }
                            catch (Exception ex)
                            {
                                // log and bail out
                                Log.Warn(ex.Message, ex, this);
                                result.Success = false;
                                result.ResultMessage = actionDefinition.GetFailureMessage();
                                failed = true;

                                break;
                            }

                        }

                        if (!failed)
                        {
                            // set successful result
                            result.Success = true;
                            result.ResultMessage = formItem.SuccessMessage;
                        }
                    }
                }
                else
                {
                    result.Success = false;
                    result.ResultMessage = "Form not found: invalid form Id";
                }
            }

            return result;
        }

        private AdaptedControlResult GetControlResult(string fieldValue, FieldItem fieldItem)
        {
            //Populate fields with values
            ControlResult controlResult = new ControlResult(fieldItem.Name, HttpUtility.UrlDecode(fieldValue), string.Empty)
            {
                FieldID = fieldItem.ID.ToString(),
                FieldName = fieldItem.Name,
                Value = HttpUtility.UrlDecode(fieldValue),
                Parameters = string.Empty
            };
            return new AdaptedControlResult(controlResult, true);
        }
    }
Step 2. Create a WebApi endpoint for clients to post to.
Now that we have the basic setup, we can create a simple API controller:

    public class WffmController : ApiController
    {
        [HttpPost]
        public IHttpActionResult Post(FormData data)
        {
            FormProcessor processor = new FormProcessor();
            FormProcessorResult result = processor.Process(data);

            if (!result.Success)
            {
                return new BadRequestErrorMessageResult(result.ResultMessage, this);
            }

            return new OkResult(this);
        }
Step 3. Register routes

var config = GlobalConfiguration.Configuration;
            config.Routes.MapHttpRoute("DefaultApiRoute",
                                     "api/{controller}/{id}",
                                     new { id = RouteParameter.Optional });

Step 4. Use with any form anywhere.

            var formFields = new List<FormField>();
            formFields.Add(new FormField
            {
                FieldName = "First Name",
                FieldValue = data.FirstName
            });
            formFields.Add(new FormField
            {
                FieldName = "Last Name",
                FieldValue = data.LastName
            });
            formFields.Add(new FormField
            {
                FieldName = "Email",
                FieldValue = data.Email
            });

            FormData formData = new FormData();
            formData.FormId = MY_WFFM_FORM_ITEM_ID; // form item id from Sitecore
            formData.Fields = formFields;



            using (WebClient client = new WebClient())
            {
                client.UploadString(RemoteUrl, "POST", JsonConvert.SerializeObject(formData));
            }

Cons:
- certain WFFM out-of-the-box features will be lost - validation! validation! validation!
- once created, the form needs to remain fairly immutable since the form item ID and the field items are the contract with any client that will submit data.

I hope that someone would find this useful the next time they're faced with a similar problem. 

Monday, March 16, 2015

Sitecore error with Lucene Thai Analyzer

ManagedPoolThread #1 2015:03:12 08:32:28 ERROR Exception
Exception: System.Reflection.TargetInvocationException
Message: Exception has been thrown by the target of an invocation.
Source: mscorlib
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at (Object , Object[] )
   at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
   at Sitecore.Jobs.Job.ThreadEntry(Object state)

Nested Exception

Exception: System.NotSupportedException
Message: PORT ISSUES
Source: Lucene.Net.Contrib.Analyzers
   at Lucene.Net.Analysis.Th.ThaiAnalyzer.ReusableTokenStream(String fieldName, TextReader reader)
   at Lucene.Net.Index.DocInverterPerField.ProcessFields(IFieldable[] fields, Int32 count)
   at Lucene.Net.Index.DocFieldProcessorPerThread.ProcessDocument()
   at Lucene.Net.Index.DocumentsWriter.UpdateDocument(Document doc, Analyzer analyzer, Term delTerm)
   at Lucene.Net.Index.IndexWriter.UpdateDocument(Term term, Document doc, Analyzer analyzer)
   at Sitecore.ContentSearch.LuceneProvider.LuceneUpdateContext.UpdateDocument(Object itemToUpdate, Object criteriaForUpdate, IExecutionContext[] executionContexts)
   at Sitecore.ContentSearch.SitecoreItemCrawler.DoUpdate(IProviderUpdateContext context, SitecoreIndexableItem indexable)
   at Sitecore.ContentSearch.LuceneProvider.LuceneIndex.PerformUpdate(IEnumerable`1 indexableUniqueIds, IndexingOptions indexingOptions)

In a single day, we saw this error appear over 9000 times on a production environment.

From what I understand (since 7.0+) Sitecore by default provides full mapping of all available Lucene.net analyzers. They are configured under:
indexConfigurations > defaultLuceneIndexConfiguration > analyzer > param desc="map"
Based on the context of the content that's indexed/searched, Sitecore will (with reflection) figure out which mapping to use. Here’s a great post explaining execution contexts - http://www.sitecore.net/learn/blogs/technical-blogs/sitecore-7-development-team/posts/2013/08/execution-contexts-explained.aspx

So the Thai Analyzer seems to be a bit broken (read not implemented) from what I see in the Lucene.Net source. The Analyzer calls the constructor for ThaiWordFilter with a token stream and that constructor just throws the exception we see. You can decompile the Lucene.Net.Contrib.Analyzers.dll or look at the source at http://lucenenet.apache.org/.

public ThaiWordFilter(TokenStream input): base(input)
{
  throw new NotSupportedException("PORT ISSUES");
  //breaker = BreakIterator.getWordInstance(new Locale("th"));
  //termAtt = AddAttribute<TermAttribute>();
  //offsetAtt = AddAttribute<OffsetAttribute>();
}

Removing or commenting out the Thai analyzer (the below mapEntry) from the execution context mappings in the Sitecore.ContentSearch.Lucene.DefaultIndexConfiguration.config should result in indexing/searching in th-TH to fall back to the standard analyzer and will get rid of the error in your log files.

             <mapEntry type="Sitecore.ContentSearch.LuceneProvider.Analyzers.PerExecutionContextAnalyzerMapEntry, Sitecore.ContentSearch.LuceneProvider">
                <param hint="executionContext" type="Sitecore.ContentSearch.CultureExecutionContext, Sitecore.ContentSearch">
                  <param hint="cultureInfo" type="System.Globalization.CultureInfo, mscorlib">
                    <param hint="name">th-TH</param>
                  </param>
                </param>
                <param desc="analyzer" type="Sitecore.ContentSearch.LuceneProvider.Analyzers.DefaultPerFieldAnalyzer, Sitecore.ContentSearch.LuceneProvider">
                  <param desc="defaultAnalyzer" type="Lucene.Net.Analysis.Th.ThaiAnalyzer, Lucene.Net.Contrib.Analyzers">
                    <param hint="version">Lucene_30</param>
                  </param>
                </param>
              </mapEntry>

If anyone has come across this before, I'd love to hear from you!


Update: Pavel Veller (@pveller) pointed out to me that this issue has been fixed with Sitecore 7.2 Update 3. As per the release notes:
  • Thai Analyzer from Lucene.Net was not fully implemented and could sometimes throw Not Supported exceptions. The analyzer has been removed from the default Lucene index configuration. The default analyzer will be used instead. (420234)

Wednesday, March 11, 2015

Searchable Language Selector

If you have ever worked in a Sitecore instance with a lot of languages, you may have noticed that sometimes it could be quite time consuming (and frustrating) to look for the language you need in the language picker. This isn't as much a developer problem as it is an issue for the content editors who often make edits in multiple languages. So, here's a quick and easy client-side solution.

The language selector is generated by an xml control located here: \sitecore\shell\Applications\Content Manager\Galleries\Languages\Gallery Languages.xml

A couple of modifications to add a search box, and a couple of javascript functions later, and we now have a searchable language selector:



You can find the modified control up on GitHub. Let me know what you guys think!

Update: This modification is now also available for download from the Sitecore Marketplace.

Thursday, February 5, 2015

Error when rendering WFFM form

I came across an interesting WFFM exception on a production CM environment today. It turned out to be a configuration error, so I decided to share
[InvalidOperationException: folder]
   Sitecore.Form.Core.Configuration.ThemesManager.GetThemeName(Item form, ID fieldID) +434
   Sitecore.Form.Core.Configuration.ThemesManager.GetThemeUrl(Item form, Boolean deviceDependant) +270
   Sitecore.Form.Core.Configuration.ThemesManager.ScriptsTags(Item form, Item contextItem) +49
   Sitecore.Form.Core.Configuration.ThemesManager.RegisterCssScript(Page page, Item form, Item contextItem) +184
   Sitecore.Form.Web.UI.Controls.SitecoreSimpleFormAscx.OnInit(EventArgs e) +233
   System.Web.UI.Control.InitRecursive(Control namingContainer) +186
   System.Web.UI.Control.AddedControl(Control control, Int32 index) +189
   Sitecore.Form.Core.Renderings.FormRender.OnInit(EventArgs e) +846
   System.Web.UI.Control.InitRecursive(Control namingContainer) +186
   System.Web.UI.Control.InitRecursive(Control namingContainer) +291
   System.Web.UI.Control.InitRecursive(Control namingContainer) +291
   System.Web.UI.Control.InitRecursive(Control namingContainer) +291
   System.Web.UI.Control.InitRecursive(Control namingContainer) +291
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +2098

The method - Sitecore.Form.Core.Configuration.ThemesManager.GetThemeName(Item form, ID fieldID) - looks at the Form ID that's configured as the Forms root ID in the site definition.
string formsRootForSite = SiteUtils.GetFormsRootForSite(Context.Site);
Item item = form;
if (form.TemplateID != IDs.FormFolderTemplateID)
{
    item = form.Database.GetItem(formsRootForSite);
}
Assert.IsNotNull(item, "folder");
In my case, the configured ID did not match the actual forms folder item ID in Sitecore.

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;
        }
    }