Home » 2013 » January

Monthly Archives: January 2013

Put Sitecore to Work for You: Build Custom Task Agents

How many times have you seen some manual process and thought to yourself how much easier your life would be easier if that process were automated?

Custom Sitecore task agents could be of assistance on achieving some automation in Sitecore.

Last night, I built a custom task agent that deletes “expired” items from the recycle bin in all three Sitecore databases — items that have been sitting in the recycle bin after a specified number of days.

I came up with this idea after remembering an instance I had seen in the past where there were so many items in the recycle bin, finding an item to restore would be more difficult than finding a needle in a haystack.

Using .NET reflector, I looked at how Sitecore.Tasks.UrlAgent in Sitecore.Kernel.dll was coded to see if I had to do anything special when building my agent — an example would be ascertaining whether I needed to inherit from a custom base class — and also looked at code in Sitecore.Shell.Applications.Archives.RecycleBin.RecycleBinPage in Sitecore.Client.dll coupled with Sitecore.Shell.Framework.Commands.Archives.Delete in Sitecore.Kernel.dll to figure out how to permanently delete items in the recycle bin.

After doing my research in those two assemblies, I came up with this custom task agent:

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

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Archiving;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Tasks
{
    public class RecycleBinCleanupAgent
    {
        private const string ArchiveName = "recyclebin";
        private static readonly char[] Delimiters = new char[] { ',', '|' };

        public IEnumerable<string> DatabaseNames { get; set; }

        private IEnumerable<Database> _Databases;
        private IEnumerable<Database> Databases
        {
            get
            {
                if (_Databases == null)
                {
                    _Databases = GetDatabases();
                }

                return _Databases;
            }
        }

        public int NumberOfDaysUntilExpiration { get; set; }

        public bool Enabled { get; set; }

        public bool LogActivity { get; set; }

        public RecycleBinCleanupAgent(string databases)
        {
            SetDatabases(databases);
        }

        private void SetDatabases(string databases)
        {
            Assert.ArgumentNotNullOrEmpty(databases, "databases");
            DatabaseNames = databases.Split(Delimiters, StringSplitOptions.RemoveEmptyEntries).Select(database => database.Trim()).ToList();
        }

        public void Run()
        {
            if (Enabled)
            {
                RemoveEntriesInAllDatabases();
            }
        }
        
        private void RemoveEntriesInAllDatabases()
        {
            DateTime expired = GetExpiredDateTime();

            if (expired == DateTime.MinValue)
            {
                return;
            }

            foreach (Database database in Databases)
            {
                RemoveEntries(database, expired);
            }
        }

        private DateTime GetExpiredDateTime()
        {
            if (NumberOfDaysUntilExpiration > 0)
            {
                return DateTime.Now.AddDays(-1 * NumberOfDaysUntilExpiration).ToUniversalTime();
            }

            return DateTime.MinValue;
        }

        private void RemoveEntries(Database database, DateTime expired)
        {
            int deletedEntriesCount = RemoveEntries(GetArchive(database), expired);
            LogInfo(deletedEntriesCount, database.Name);
        }

        private static int RemoveEntries(Archive archive, DateTime expired)
        {
            IEnumerable<ArchiveEntry> archiveEntries = GetAllEntries(archive);
            int deletedEntriesCount = 0;

            foreach (ArchiveEntry archiveEntry in archiveEntries)
            {
                if (ShouldDeleteEntry(archiveEntry, expired))
                {
                    archive.RemoveEntries(CreateNewArchiveQuery(archiveEntry));
                    deletedEntriesCount++;
                }
            }

            return deletedEntriesCount;
        }

        private static IEnumerable<ArchiveEntry> GetAllEntries(Archive archive)
        {
            Assert.ArgumentNotNull(archive, "archive");
            return archive.GetEntries(0, archive.GetEntryCount()); ;
        }

        private static bool ShouldDeleteEntry(ArchiveEntry archiveEntry, DateTime expired)
        {
            Assert.ArgumentNotNull(archiveEntry, "archiveEntry");
            Assert.ArgumentCondition(expired > DateTime.MinValue, "expired", "expired must be set!");
            return archiveEntry.ArchiveDate <= expired;
        }

        private static ArchiveQuery CreateNewArchiveQuery(ArchiveEntry archiveEntry)
        {
            Assert.ArgumentNotNull(archiveEntry, "archiveEntry");
            return CreateNewArchiveQuery(archiveEntry.ArchivalId);
        }

        private static ArchiveQuery CreateNewArchiveQuery(Guid archivalId)
        {
            Assert.ArgumentCondition(archivalId != Guid.Empty, "archivalId", "archivalId must be set!");
            return new ArchiveQuery { ArchivalId = archivalId };
        }

        private void LogInfo(int deletedEntriesCount, string databaseName)
        {
            bool canLogInfo = LogActivity
                              && deletedEntriesCount > 0 
                              && !string.IsNullOrEmpty(databaseName);

            if (canLogInfo)
            {
                Log.Info(CreateNewLogEntry(deletedEntriesCount, databaseName, ArchiveName), this);
            }
        }

        private static string CreateNewLogEntry(int expiredEntryCount, string databaseName, string archiveName)
        {
            return string.Format("{0} expired archive entries permanently deleted (database: {1}, archive: {2})", expiredEntryCount, databaseName, archiveName);
        }

        private IEnumerable<Database> GetDatabases()
        {
            if (DatabaseNames != null)
            {
                return DatabaseNames.Select(database => Factory.GetDatabase(database)).ToList();
            }

            return new List<Database>();
        }

        private static Archive GetArchive(Database database)
        {
            Assert.ArgumentNotNull(database, "database");
            return ArchiveManager.GetArchive(ArchiveName, database);
        }
    }
}

Basically, it loops over all recycle bin archived entries in all specified databases after an allotted time interval — the time interval and target databases are set in a patch config file you will see below — and removes an entry when its archival date is older than the minimum expiration date — a date I derive from the NumberOfDaysUntilExpiration setting:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <scheduling>
      <agent type="Sitecore.Sandbox.Tasks.RecycleBinCleanupAgent, Sitecore.Sandbox" method="Run" interval="01:00:00">
        <param desc="databases">core, master, web</param>
        <NumberOfDaysUntilExpiration>30</NumberOfDaysUntilExpiration>
        <LogActivity>true</LogActivity>
        <Enabled>true</Enabled>
      </agent>
    </scheduling>
  </sitecore>
</configuration>

I had 89 items in my master database’s recycle bin before my task agent ran:

before-recycle-bin-agent-runs-master

I walked away for a bit to watch some television, eat dinner, and surf Twitter for a bit, and phone my brother. I then returned to see the following in my Sitecore log:

after-recycle-bin-agent-runs-master-log

I went into the recycle bin in my master database, and saw there were 5 items left after my task agent executed:

after-recycle-bin-agent-runs-master

As you can see, my custom task agent deleted expired recycle bin items as designed. 🙂

Advertisement

Who Just Published That? Log Publishing Statistics in the Sitecore Client

Time and time again, I keep hearing about content in Sitecore being published prematurely, and this usually occurs by accident — perhaps a cat walks across the malefactor’s keyboard — although I have a feeling something else might driving these erroneous publishes.

However, in some instances, the malefactor will not fess up to invoking said publish.

Seeking out who had published a Sitecore item is beyond the know how of most end users, and usually requires an advanced user or developer to fish around in the Sitecore log to ascertain who published what and when.

Here’s an example of publishing information in my local sandbox’s instance of a publish I had done earlier this evening:

publishing-log-file

Given that I keep hearing about this happening, I decided it would be a great exercise to develop a feature to bring publishing information into the Sitecore client. We can accomplish this by building a custom PublishItemProcessor pipeline to log statistics into publishing fields.

First, I created a template containing fields to hold publishing information.

publishing-statistics-template

These fields will only keep track of who published an item last, similar to how the fields in the Statistics section in the Standard Template.

Next, I set the title of my fields to not show underscores:

set-title-field-no-underscores

I then added my template to the Standard Template:

add-publishing-stats-standard-template

Now that my publishing fields are in Sitecore, It’s time to build a custom PublishItemProcessor (Sitecore.Publishing.Pipelines.PublishItem.PublishItemProcessor):

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

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Fields;
using Sitecore.Diagnostics;
using Sitecore.Publishing.Pipelines.PublishItem;
using Sitecore.SecurityModel;

namespace Sitecore.Sandbox.Pipelines.Publishing
{
    public class UpdatePublishingStatistics : PublishItemProcessor
    {
        private const string PublishedFieldName = "__Published";
        private const string PublishedByFieldName = "__Published By";

        public override void Process(PublishItemContext context)
        {
            SetPublishingStatisticsFields(context);
        }

        private void SetPublishingStatisticsFields(PublishItemContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.PublishOptions, "context.PublishOptions");
            Assert.ArgumentNotNull(context.PublishOptions.SourceDatabase, "context.PublishOptions.SourceDatabase");
            Assert.ArgumentNotNull(context.PublishOptions.TargetDatabase, "context.PublishOptions.TargetDatabase");
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(context.ItemId), "context.ItemId", "context.ItemId must be set!");
            Assert.ArgumentNotNull(context.User, "context.User");
            
            SetPublishingStatisticsFields(context.PublishOptions.SourceDatabase, context.ItemId, context.User.Name);
            SetPublishingStatisticsFields(context.PublishOptions.TargetDatabase, context.ItemId, context.User.Name);
        }

        private void SetPublishingStatisticsFields(Database database, ID itemId, string userName)
        {
            Assert.ArgumentNotNull(database, "database");
            Item item = TryGetItem(database, itemId);

            if (HasPublishingStatisticsFields(item))
            {
                SetPublishingStatisticsFields(item, DateUtil.IsoNow, userName);
            }
        }

        private void SetPublishingStatisticsFields(Item item, string isoDateTime, string userName)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNullOrEmpty(isoDateTime, "isoDateTime");
            Assert.ArgumentNotNullOrEmpty(userName, "userName");

            using (new SecurityDisabler())
            {
                item.Editing.BeginEdit();
                item.Fields[PublishedFieldName].Value = DateUtil.IsoNow;
                item.Fields[PublishedByFieldName].Value = userName;
                item.Editing.EndEdit();
            }
        }

        private Item TryGetItem(Database database, ID itemId)
        {
            try
            {
                return database.Items[itemId];
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }

            return null;
        }

        private static bool HasPublishingStatisticsFields(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item.Fields[PublishedFieldName] != null
                    && item.Fields[PublishedByFieldName] != null;
        }
    }
}

My PublishItemProcessor sets the publishing statistics on my newly added fields on the item in both the source and target databases, and only when the publishing fields exist on the published item.

I then added my PublishItemProcessor after the UpdateStatistics PublishItemProcessor in a new patch config file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <publishItem>
        <processor type="Sitecore.Sandbox.Pipelines.Publishing.UpdatePublishingStatistics, Sitecore.Sandbox"
					patch:after="processor[@type='Sitecore.Publishing.Pipelines.PublishItem.UpdateStatistics, Sitecore.Kernel']" />
      </publishItem>
    </pipelines>
  </sitecore>
</configuration>

I created a new page for testing, put some dummy data in some fields and then published:

publishing-stats-after-publish

Beware — you can no longer hide after publishing when you shouldn’t! 🙂

Manipulate Field Values in a Custom Sitecore Web Forms for Marketers DataProvider

In my Experiments with Field Data Encryption in Sitecore article, I briefly mentioned designing a custom Web Forms for Marketers (WFFM) DataProvider that uses the decorator pattern for encrypting and decrypting field values before saving and retrieving field values from the WFFM database.

From the time I penned that article up until now, I have been feeling a bit guilty that I may have left you hanging by not going into the mechanics around how I did that — I built that DataProvider for my company to be used in one of our healthcare specific content management modules.

To make up for not showing you this, I decided to build another custom WFFM DataProvider — one that will replace periods with smiley faces and the word “Sitecore” with “Sitecore®”, case insensitively.

First, I defined an interface for utility classes that will manipulate objects for us. All manipulators will consume an object of a specified type, manipulate that object in some way, and then return the maniputed object to the manipulator object’s client:

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IManipulator<T>
    {
        T Manipulate(T source);
    }
}

The first manipulator I built is a string manipulator. Basically, this object will take in a string and replace a specified substring with another:

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IStringReplacementManipulator : IManipulator<string>
    {
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.Manipulators.Base;

namespace Sitecore.Sandbox.Utilities.Manipulators
{
    public class StringReplacementManipulator : IStringReplacementManipulator
    {
        private string PatternToReplace { get; set; }
        private string ReplacementString { get; set; }
        private bool IgnoreCase { get; set; }

        private StringReplacementManipulator(string patternToReplace, string replacementString)
            : this(patternToReplace, replacementString, false)
        {
        }

        private StringReplacementManipulator(string patternToReplace, string replacementString, bool ignoreCase)
        {
            SetPatternToReplace(patternToReplace);
            SetReplacementString(replacementString);
            SetIgnoreCase(ignoreCase);
        }

        private void SetPatternToReplace(string patternToReplace)
        {
            Assert.ArgumentNotNullOrEmpty(patternToReplace, "patternToReplace");
            PatternToReplace = patternToReplace;
        }

        private void SetReplacementString(string replacementString)
        {
            ReplacementString = replacementString;
        }

        private void SetIgnoreCase(bool ignoreCase)
        {
            IgnoreCase = ignoreCase;
        }

        public string Manipulate(string source)
        {
            Assert.ArgumentNotNullOrEmpty(source, "source");
            RegexOptions regexOptions = RegexOptions.None;

            if (IgnoreCase)
            {
                regexOptions = RegexOptions.IgnoreCase;
            }

            return Regex.Replace(source, PatternToReplace, ReplacementString, regexOptions);
        }

        public static IStringReplacementManipulator CreateNewStringReplacementManipulator(string patternToReplace, string replacementString)
        {
            return new StringReplacementManipulator(patternToReplace, replacementString);
        }

        public static IStringReplacementManipulator CreateNewStringReplacementManipulator(string patternToReplace, string replacementString, bool ignoreCase)
        {
            return new StringReplacementManipulator(patternToReplace, replacementString, ignoreCase);
        }
    }
}

Clients of this class can choose to have substrings replaced in a case sensitive or insensitive manner.

By experimenting in a custom WFFM DataProvider and investigating code via .NET reflector in the WFFM assemblies, I discovered I had to create a custom object that implements Sitecore.Forms.Data.IField.

Out of the box, WFFM uses Sitecore.Forms.Data.DefiniteField — a class that implements Sitecore.Forms.Data.IField, albeit this class is declared internal and cannot be reused outside of the Sitecore.Forms.Core.dll assembly.

When I attempted to change the Value property of this object in a custom WFFM DataProvider, changes did not stick for some reason — a reason that I have not definitely ascertained.

However, to get around these lost Value property changes, I created a custom object that implements Sitecore.Forms.Data.IField:

using System;

using Sitecore.Forms.Data;

namespace Sitecore.Sandbox.Utilities.Manipulators.DTO
{
    public class WFFMField : IField
    {
        public string Data { get; set; }

        public Guid FieldId { get; set; }

        public string FieldName { get; set; }

        public IForm Form { get; set; }

        public Guid Id { get; internal set; }

        public string Value { get; set; }
    }
}

Next, I built a WFFM Field collection manipulator — an IEnumerable of Sitecore.Forms.Data.IField defined in Sitecore.Forms.Core.dll — using the DTO defined above:

using System.Collections.Generic;

using Sitecore.Forms.Data;

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IWFFMFieldsManipulator : IManipulator<IEnumerable<IField>>
    {
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Diagnostics;
using Sitecore.Forms.Data;

using Sitecore.Sandbox.Utilities.Manipulators.Base;
using Sitecore.Sandbox.Utilities.Manipulators.DTO;

namespace Sitecore.Sandbox.Utilities.Manipulators
{
    public class WFFMFieldsManipulator : IWFFMFieldsManipulator
    {
        private IEnumerable<IStringReplacementManipulator> FieldValueManipulators { get; set; }

        private WFFMFieldsManipulator(params IStringReplacementManipulator[] fieldValueManipulator)
            : this(fieldValueManipulator.AsEnumerable())
        {
        }

        private WFFMFieldsManipulator(IEnumerable<IStringReplacementManipulator> fieldValueManipulators)
        {
            SetFieldValueManipulator(fieldValueManipulators);
        }

        private void SetFieldValueManipulator(IEnumerable<IStringReplacementManipulator> fieldValueManipulators)
        {
            Assert.ArgumentNotNull(fieldValueManipulators, "fieldValueManipulators");
            foreach (IStringReplacementManipulator fieldValueManipulator in fieldValueManipulators)
            {
                Assert.ArgumentNotNull(fieldValueManipulator, "fieldValueManipulator");
            }

            FieldValueManipulators = fieldValueManipulators;
        }

        public IEnumerable<IField> Manipulate(IEnumerable<IField> fields)
        {
            IList<IField> maniuplatdFields = new List<IField>();

            foreach (IField field in fields)
            {
                maniuplatdFields.Add(MainpulateFieldValue(field));
            }

            return maniuplatdFields;
        }

        private IField MainpulateFieldValue(IField field)
        {
            IField maniuplatedField = CreateNewWFFMField(field);

            if (maniuplatedField != null)
            {
                maniuplatedField.Value = ManipulateString(maniuplatedField.Value);
            }

            return maniuplatedField;
        }

        private static IField CreateNewWFFMField(IField field)
        {
            if(field != null)
            {
                return new WFFMField
                {
                    Data = field.Data,
                    FieldId = field.FieldId,
                    FieldName = field.FieldName,
                    Form = field.Form,
                    Id = field.Id,
                    Value = field.Value
                };
            }

            return null;
        }

        private string ManipulateString(string stringToManipulate)
        {
            if (string.IsNullOrEmpty(stringToManipulate))
            {
                return string.Empty;
            }

            string manipulatedString = stringToManipulate;

            foreach(IStringReplacementManipulator fieldValueManipulator in FieldValueManipulators)
            {
                manipulatedString = fieldValueManipulator.Manipulate(manipulatedString);
            }

            return manipulatedString;
        }

        public static IWFFMFieldsManipulator CreateNewWFFMFieldsManipulator(params IStringReplacementManipulator[] fieldValueManipulators)
        {
            return new WFFMFieldsManipulator(fieldValueManipulators);
        }

        public static IWFFMFieldsManipulator CreateNewWFFMFieldsManipulator(IEnumerable<IStringReplacementManipulator> fieldValueManipulators)
        {
            return new WFFMFieldsManipulator(fieldValueManipulators);
        }
    }
}

This manipulator consumes a collection of string manipulators and delegates to these for making changes to WFFM field values.

Now, it’s time to create a custom WFFM DataProvider that uses our manipulators defined above.

In my local sandbox Sitecore instance, I’m using SQLite for my WFFM module, and must decorate an instance of Sitecore.Forms.Data.DataProviders.SQLite.SQLiteWFMDataProvider — although this approach would be the same using MS SQL or Oracle since all WFFM DataProviders should inherit from the abstract class Sitecore.Forms.Data.DataProviders.WFMDataProviderBase:

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

using Sitecore.Diagnostics;
using Sitecore.Forms.Data;
using Sitecore.Forms.Data.DataProviders;
using Sitecore.Forms.Data.DataProviders.SQLite;

using Sitecore.Sandbox.Translation.Base;
using Sitecore.Sandbox.Utilities.Manipulators.Base;
using Sitecore.Sandbox.Utilities.Manipulators;

namespace Sitecore.Sandbox.WFFM.Data.DataProviders
{
    public class FieldValueManipulationSQLiteWFMDataProvider : WFMDataProviderBase
    {
        private static readonly IStringReplacementManipulator RegisteredTrademarkManipulator = StringReplacementManipulator.CreateNewStringReplacementManipulator("sitecore", " Sitecore®", true);
        private static readonly IStringReplacementManipulator PeriodsToSmiliesManipulator = StringReplacementManipulator.CreateNewStringReplacementManipulator("\\.", " :)");
        private static readonly IWFFMFieldsManipulator FieldsManipulator = WFFMFieldsManipulator.CreateNewWFFMFieldsManipulator(RegisteredTrademarkManipulator, PeriodsToSmiliesManipulator);

	    private WFMDataProviderBase InnerProvider { get; set; }

        public FieldValueManipulationSQLiteWFMDataProvider() 
		    : this(CreateNewSQLiteWFMDataProvider())
        {
        }

        public FieldValueManipulationSQLiteWFMDataProvider(string connectionString)
		    : this(CreateNewSQLiteWFMDataProvider(connectionString))
        {
        }

        public FieldValueManipulationSQLiteWFMDataProvider(WFMDataProviderBase innerProvider)
        {
		    SetInnerProvider(innerProvider);
	    }
	
	    private void SetInnerProvider(WFMDataProviderBase innerProvider)
	    {
		    Assert.ArgumentNotNull(innerProvider, "innerProvider");
		    InnerProvider = innerProvider;
	    }

        private static WFMDataProviderBase CreateNewSQLiteWFMDataProvider()
	    {
            return new SQLiteWFMDataProvider();
	    }

        private static WFMDataProviderBase CreateNewSQLiteWFMDataProvider(string connectionString)
	    {
		    Assert.ArgumentNotNullOrEmpty(connectionString, "connectionString");
            return new SQLiteWFMDataProvider(connectionString);
	    }

        public override void ChangeStorage(Guid formItemId, string newStorage)
        {
            InnerProvider.ChangeStorage(formItemId, newStorage);
        }

        public override void ChangeStorageForForms(IEnumerable<Guid> ids, string storageName)
        {
            InnerProvider.ChangeStorageForForms(ids, storageName);
        }

        public override void DeleteForms(IEnumerable<Guid> formSubmitIds)
        {
            InnerProvider.DeleteForms(formSubmitIds);
        }

        public override void DeleteForms(Guid formItemId, string storageName)
        {
            InnerProvider.DeleteForms(formItemId, storageName);
        }

        public override IEnumerable<IPool> GetAbundantPools(Guid fieldId, int top, out int total)
        {
            return InnerProvider.GetAbundantPools(fieldId, top, out total);
        }

        public override IEnumerable<IForm> GetForms(QueryParams queryParams, out int total)
        {
            return InnerProvider.GetForms(queryParams, out total);
        }

        public override IEnumerable<IForm> GetFormsByIds(IEnumerable<Guid> ids)
        {
            return InnerProvider.GetFormsByIds(ids);
        }

        public override int GetFormsCount(Guid formItemId, string storageName, string filter)
        {
            return InnerProvider.GetFormsCount(formItemId, storageName, filter);
        }

        public override IEnumerable<IPool> GetPools(Guid fieldId)
        {
            return InnerProvider.GetPools(fieldId);
        }

        public override void InsertForm(IForm form)
        {
            ManipulateFields(form);
            InnerProvider.InsertForm(form);
        }

        public override void ResetPool(Guid fieldId)
        {
            InnerProvider.ResetPool(fieldId);
        }

        public override IForm SelectSingleForm(Guid fieldId, string likeValue)
        {
            return InnerProvider.SelectSingleForm(fieldId, likeValue);
        }

        public override bool UpdateForm(IForm form)
        {
            ManipulateFields(form);
            return InnerProvider.UpdateForm(form);
        }

        private static void ManipulateFields(IForm form)
        {
            Assert.ArgumentNotNull(form, "form");
            Assert.ArgumentNotNull(form.Field, "form.Field");
            form.Field = FieldsManipulator.Manipulate(form.Field);
        }
    }
}

This custom DataProvider creates an instance of Sitecore.Forms.Data.DataProviders.SQLite.SQLiteWFMDataProvider and delegates method calls to it.

However, before delegating to insert and update method calls, forms fields are manipulated via our manipulators objects — our manipulators will replace periods with smiley faces, and the word “Sitecore” with “Sitecore®”.

I then had to configure WFFM to use my custom DataProvider above in /App_Config/Include/forms.config:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
	<sitecore>
	
		<!-- A bunch of stuff here -->
		
		<!-- SQLite -->
		<formsDataProvider type="Sitecore.Sandbox.WFFM.Data.DataProviders.FieldValueManipulationSQLiteWFMDataProvider,Sitecore.Sandbox">
			<param desc="connection string">Data Source=/data/sitecore_webforms.db;version=3;BinaryGUID=true</param>
		</formsDataProvider>

		<!-- A bunch of stuff here -->
	
	</sitecore>
</configuration>

For testing, I build a random WFFM form containing three fields, and created a page item to hold this form.

I then navigated to my form page and filled it in:

random-form-before-sumbit

I then clicked the Submit button:

random-form-after-submit

I then opened up the Form Reports for my form:

random-form-reports

As you can see, it all gelled together nicely. 🙂

Get Hooked on Hooks: Create a Custom Hook in Sitecore

Yesterday evening, I decided to fish around in my local Sitecore instance’s Web.config to look for customization opportunities — doing this definitely beats vegging out in front of the television any day — and thought it would be an interesting exercise to create a custom hook.

Before I dive into the custom hook I built, I’d like to discuss what a Sitecore hook is.

You can consider a hook to be an object containing code that you would like executed in your Sitecore instance, but see no logical place to put said code.

You’re probably saying to yourself “Mike, that is an extremely generic and ridiculous definition.” I don’t blame you for thinking this, although I can’t really define what a hook is any better than that. If you have a better definition, please leave a comment.

However, to put things into context that might aid in defining what a hook could be, a hook is usually defined as an object containing code that is executed periodically by a defined configuration setting time interval — albeit this isn’t a mandatory constraint since one could inject any code via a hook, as long as that hook implements the Sitecore.Events.Hooks.IHook interface.

This interface defines one method — the Initialize() method — which has a very simple signature: it takes in no parameters and does not return anything, thus giving you lots of freedom around how you implement your hook.

Out of the box, Sitecore employs two hooks:

  1. Sitecore.Diagnostics.HealthMonitorHook – a hook that launches a pipeline periodically to log cache, memory, and performance counter information to the Sitecore log.
  2. Sitecore.Diagnostics.MemoryMonitorHook – a hook that monitors memory periodically on the server, and clears caches/invokes the garbage collector when a defined thresholds are exceeded and settings defined in the Web.config allow for these actions.

Both hooks are defined in Sitecore.Kernel.dll.

Keeping with the monitoring theme of the prepackaged hooks, I decided to build a hook that monitors the size of my Sitecore databases after an elapsed period of time, and logs this information into my Sitecore log.

First, I created a data transfer object (DTO) that represents a size snapshot of a database:

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

namespace Sitecore.Sandbox.Utilities.Database.DTO
{
    public class DatabaseStatistics
    {
        public string Name { get; set; }
        public string Size { get; set; }
        public string UnallocatedSpace { get; set; }
    }
}

The above DTO is created and returned by a class that gets information out of a database using the Sitecore.Data.SqlServer.SqlServerDataApi utility class used by Sitecore for database operations in MS SQL Server:

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

using Sitecore.Sandbox.Utilities.Database.DTO;

namespace Sitecore.Sandbox.Utilities.Database.Base
{
    public interface IDatabaseStatisticsGatherer
    {
        DatabaseStatistics GetDatabaseStatistics();
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Data.DataProviders.Sql;
using Sitecore.Data.SqlServer;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.Database.Base;
using Sitecore.Sandbox.Utilities.Database.DTO;

namespace Sitecore.Sandbox.Utilities.Database
{
    public class DatabaseStatisticsGatherer : IDatabaseStatisticsGatherer
    {
        private const string GetSizeStaticsSQL = "exec sp_spaceused"; // this sproc gives database size information

        private SqlDataApi SqlDataApi { get; set; }

        private DatabaseStatisticsGatherer(string connectionString)
            : this(CreateNewSqlServerDataApi(connectionString))
        {
        }

        private DatabaseStatisticsGatherer(SqlDataApi sqlDataApi)
        {
            SetSqlDataApi(sqlDataApi);
        }

        private void SetSqlDataApi(SqlDataApi sqlDataApi)
        {
            Assert.ArgumentNotNull(sqlDataApi, "sqlDataApi");
            SqlDataApi = sqlDataApi;
        }

        public DatabaseStatistics GetDatabaseStatistics()
        {
            IEnumerable<string> columnValues = SqlDataApi.GetStringList(GetSizeStaticsSQL, new object[0]);
            return CreateNewDatabaseStatistics(columnValues);
        }

        private static DatabaseStatistics CreateNewDatabaseStatistics(IEnumerable<string> columnValues)
        {
            if (columnValues == null || columnValues.Count() < 1)
            {
                return null;
            }

            return new DatabaseStatistics
            {
                Name = columnValues.ElementAtOrDefault(0),
                Size = columnValues.ElementAtOrDefault(1),
                UnallocatedSpace = columnValues.ElementAtOrDefault(2)
            };
        }

        private static SqlDataApi CreateNewSqlServerDataApi(string connectionString)
        {
            Assert.ArgumentNotNullOrEmpty(connectionString, "connectionString");
            return new SqlServerDataApi(connectionString);
        }

        public static IDatabaseStatisticsGatherer CreateNewDatabaseStatisticsGatherer(string connectionString)
        {
            return new DatabaseStatisticsGatherer(connectionString);
        }

        public static IDatabaseStatisticsGatherer CreateNewDatabaseStatisticsGatherer(SqlDataApi sqlDataApi)
        {
            return new DatabaseStatisticsGatherer(sqlDataApi);
        }
    }
}

I decided to follow the paradigm set forth by Sitecore.Diagnostics.HealthMonitorHook: having my hook invoke a pipeline after an elapsed period of time. Here are this pipeline’s DTO and the pipeline class itself:

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

using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.DatabaseMonitor.DTO
{
    public class DatabaseMonitorArgs : PipelineArgs
    {
        public IEnumerable<string> Databases { get; set; }
    }
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;

using Sitecore.Collections;
using Sitecore.Data.Sql;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Pipelines.DatabaseMonitor.DTO;
using Sitecore.Sandbox.Utilities.Database;
using Sitecore.Sandbox.Utilities.Database.Base;
using Sitecore.Sandbox.Utilities.Database.DTO;

namespace Sitecore.Sandbox.Pipelines.DatabaseMonitor
{
    public class DatabaseMonitor
    {
        public void LogDatabasesSize(DatabaseMonitorArgs args)
        {
            foreach (string connectionStringKey in args.Databases)
            {
                LogDatabaseStatistics(connectionStringKey);
            }
        }

        private void LogDatabaseStatistics(string connectionStringKey)
        {
            LogDatabaseStatistics(GetDatabaseStatistics(connectionStringKey));
        }

        private static DatabaseStatistics GetDatabaseStatistics(string connectionStringKey)
        {
            IDatabaseStatisticsGatherer gatherer = DatabaseStatisticsGatherer.CreateNewDatabaseStatisticsGatherer(GetConnectionString(connectionStringKey));
            return gatherer.GetDatabaseStatistics();
        }

        private void LogDatabaseStatistics(DatabaseStatistics statistics)
        {
            if (statistics == null)
            {
                return;
            }

            Log.Info(GetLogEntry(statistics), this);
        }

        private static string GetConnectionString(string connectionStringKey)
        {
            Assert.ArgumentNotNullOrEmpty(connectionStringKey, "connectionStringKey");
            return ConfigurationManager.ConnectionStrings[connectionStringKey].ConnectionString;
        }

        private static string GetLogEntry(DatabaseStatistics statistics)
        {
            Assert.ArgumentNotNull(statistics, "statistics");
            return string.Format("Database size statistics: '{0}' (size: {1}, unallocated space: {2})", statistics.Name, statistics.Size, statistics.UnallocatedSpace);
        }
    }
}

Now, it’s time to hookup my hook. I followed how Sitecore.Diagnostics.HealthMonitorHook uses the AlarmClock class to continuously invoke code after a specified period of time:

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

using Sitecore.Diagnostics;
using Sitecore.Events.Hooks;
using Sitecore.Pipelines;
using Sitecore.Services;
using Sitecore.Text;

using Sitecore.Sandbox.Pipelines.DatabaseMonitor.DTO;

namespace Sitecore.Sandbox.Hooks
{
    public class DatabaseMonitorHook : IHook
    {
        private static readonly char[] Delimiters = new char[] { ',', '|' };
        private static AlarmClock _alarmClock;

        private IEnumerable<string> Databases { get; set; }
        private TimeSpan Interval { get; set; }
        private bool Enabled { get; set; }

        public DatabaseMonitorHook(string databases, string interval, string enabled)
        {
            SetDatabases(databases);
            SetInterval(interval);
            SetEnabled(enabled);
        }

        private void SetDatabases(string databases)
        {
            Assert.ArgumentNotNullOrEmpty(databases, "databases");
            Databases = databases.Split(Delimiters, StringSplitOptions.RemoveEmptyEntries).Select(database => database.Trim());
        }

        private void SetInterval(string interval)
        {
            Assert.ArgumentNotNullOrEmpty(interval, "interval");
            Interval = TimeSpan.Parse(interval);
        }

        private void SetEnabled(string enabled)
        {
            bool isEnabled;
            if (bool.TryParse(enabled, out isEnabled))
            {
                Enabled = isEnabled;
            }
        }

        private void AlarmClock_Ring(object sender, EventArgs args)
        {
            Pipeline.Start("databaseMonitor", CreateNewDatabaseMonitorArgs());
        }

        private DatabaseMonitorArgs CreateNewDatabaseMonitorArgs()
        {
            return new DatabaseMonitorArgs { Databases = Databases };
        }

        public void Initialize()
        {
            if (Enabled && _alarmClock == null)
            {
                _alarmClock = CreateNewAlarmClock(Interval);
                _alarmClock.Ring += new EventHandler<EventArgs>(AlarmClock_Ring);
            }
        }

        private static AlarmClock CreateNewAlarmClock(TimeSpan interval)
        {
            return new AlarmClock(interval);
        }
    }
}

I glued everything together via a new config file where I define my new hook and pipeline:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <hooks>
      <hook type="Sitecore.Sandbox.Hooks.DatabaseMonitorHook, Sitecore.Sandbox">
        <param desc="databases">core, master, web</param>
        <param desc="interval">00:01:00</param>
        <param desc="enabled">true</param>
      </hook>
    </hooks>
    <processors>
      <databaseMonitor>
        <processor type="Sitecore.Sandbox.Pipelines.DatabaseMonitor.DatabaseMonitor, Sitecore.Sandbox" method="LogDatabasesSize"/>
      </databaseMonitor>
    </processors>
  </sitecore>
</configuration>

After all of the above code was compiled and the configuration file was saved, I kick-started my local Sitecore instance by navigating to the home page of my site. I then walked away for a bit. When I returned, I opened up my most recent log file and saw the following:

size-stats-logged

As shown in my log file entries, my hook along with its supporting classes were all hooked up correctly. 🙂

Rip Out Sitecore Web Forms for Marketers Field Values During a Custom Save Action

A couple of weeks ago, I architected a Web Forms for Marketers (WFFM) solution that sends credit card information to a third-party credit card processor — via a custom form verification step — and blanks out sensitive credit card information before saving into the WFFM database.

Out of the box, WFFM will save credit card information in plain text to its database — yes, the data is hidden in the form report, although is saved to the database as plain text. This information being saved as plain text is a security risk.

One could create a solution similar to what I had done in my article discussing data encryption — albeit it might be in your best interest to avoid any headaches and potential security issues around saving and holding onto credit card information.

I can’t share the solution I built a couple of weeks ago — I built it for my current employer. I will, however, share a similar solution where I blank out the value of a field containing a social security number — a unique identifier of a citizen of the United States — of a person posing a question to the Internal Revenue Service (IRS) of the United States (yes, I chose this topic since tax season is upon us here in the US, and I loathe doing taxes :)).

First, I created a custom Web Forms for Marketers field for social security numbers. It is just a composite control containing three textboxes separated by labels containing hyphens:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;

using Sitecore.Form.Core.Attributes;
using Sitecore.Form.Core.Controls.Data;
using Sitecore.Form.Core.Visual;
using Sitecore.Form.Web.UI.Controls;

namespace Sitecore.Sandbox.WFFM.Controls
{
    public class SocialSecurityNumber : InputControl
    {
        private const string Hyphen = "-";
        private static readonly string baseCssClassName = "scfSingleLineTextBorder";
        protected TextBox firstPart;
        protected System.Web.UI.WebControls.Label firstMiddleSeparator;
        protected TextBox middlePart;
        protected System.Web.UI.WebControls.Label middleLastSeparator;
        protected TextBox lastPart;

        public SocialSecurityNumber() : this(HtmlTextWriterTag.Div)
        {
            firstPart = new TextBox();
            firstMiddleSeparator = new System.Web.UI.WebControls.Label { Text = Hyphen };
            middlePart = new TextBox();
            middleLastSeparator = new System.Web.UI.WebControls.Label { Text = Hyphen };
            lastPart = new TextBox();
        }

        public SocialSecurityNumber(HtmlTextWriterTag tag)
            : base(tag)
        {
            this.CssClass = baseCssClassName;
        }

        protected override void OnInit(EventArgs e)
        {
            SetCssClasses();
            SetTextBoxeWidths();
            SetMaxLengths();
            SetTextBoxModes();
            AddChildControls();
        }

        private void SetCssClasses()
        {
            help.CssClass = "scfSingleLineTextUsefulInfo";
            title.CssClass = "scfSingleLineTextLabel";
            generalPanel.CssClass = "scfSingleLineGeneralPanel";
        }

        private void SetTextBoxeWidths()
        {
            firstPart.Style.Add("width", "40px");
            middlePart.Style.Add("width", "35px");
            lastPart.Style.Add("width", "50px");
        }

        private void SetMaxLengths()
        {
            firstPart.MaxLength = 3;
            middlePart.MaxLength = 2;
            lastPart.MaxLength = 4;
        }

        private void SetTextBoxModes()
        {
            firstPart.TextMode = TextBoxMode.SingleLine;
            middlePart.TextMode = TextBoxMode.SingleLine;
            lastPart.TextMode = TextBoxMode.SingleLine;
        }

        private void AddChildControls()
        {
            Controls.AddAt(0, generalPanel);
            Controls.AddAt(0, title);
            
            generalPanel.Controls.Add(firstPart);
            generalPanel.Controls.Add(firstMiddleSeparator);
            generalPanel.Controls.Add(middlePart);
            generalPanel.Controls.Add(middleLastSeparator);
            generalPanel.Controls.Add(lastPart);
            generalPanel.Controls.Add(help);
        }

        public override string ID
        {
            get
            {
                return firstPart.ID;
            }
            set
            {
                title.ID = string.Concat(value, "_text");
                firstPart.ID = value;
                base.ID = string.Concat(value, "_scope");
                title.AssociatedControlID = firstPart.ID;
            }
        }

        public override ControlResult Result
        {
            get
            {
                return GetNewControlResult();
            }
        }

        private ControlResult GetNewControlResult()
        {
            TrimAllTextBoxes();
            return new ControlResult(ControlName, GetValue(), string.Empty);
        }

        private string GetValue()
        {
            bool hasValue = !string.IsNullOrEmpty(firstPart.Text) 
                            && !string.IsNullOrEmpty(middlePart.Text) 
                            && !string.IsNullOrEmpty(lastPart.Text);

            if (hasValue)
            {
                return string.Concat(firstPart.Text, firstMiddleSeparator.Text, middlePart.Text, middleLastSeparator.Text, lastPart.Text);
            }

            return string.Empty;
        }

        private void TrimAllTextBoxes()
        {
            firstPart.Text = firstPart.Text.Trim();
            middlePart.Text = middlePart.Text.Trim();
            lastPart.Text = lastPart.Text.Trim();
        }
    }
}

I then registered this custom field in Sitecore. I added my custom field item under /sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types/Custom:

registered-ssn-field

Next, I decided to create a generic interface that defines objects that will excise or rip out values from a given object of a certain type, and returns a new object of another type — although both types could be the same:

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

namespace Sitecore.Sandbox.Utilities.Excisors.Base
{
    public interface IExcisor<T, U>
    {
        U Excise(T source);
    }
}

My WFFM excisor will take in a collection of WFFM fields and return a new collection where the targeted field values — field values that I don’t want saved into the WFFM database — are set to the empty string.

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

using Sitecore.Form.Core.Controls.Data;
using Sitecore.Form.Core.Client.Data.Submit;

namespace Sitecore.Sandbox.Utilities.Excisors.Base
{
    public interface IWFFMFieldValuesExcisor : IExcisor<AdaptedResultList, AdaptedResultList>
    {
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Diagnostics;
using Sitecore.Form.Core.Client.Data.Submit;
using Sitecore.Form.Core.Controls.Data;

using Sitecore.Sandbox.Utilities.Excisors.Base;

namespace Sitecore.Sandbox.Utilities.Excisors
{
    public class WFFMFieldValuesExcisor : IWFFMFieldValuesExcisor
    {
        private IEnumerable<string> FieldNamesForExtraction { get; set; }

        private WFFMFieldValuesExcisor(IEnumerable<string> fieldNamesForExtraction)
        {
            SetFieldNamesForExtraction(fieldNamesForExtraction);
        }

        private void SetFieldNamesForExtraction(IEnumerable<string> fieldNamesForExtraction)
        {
            Assert.ArgumentNotNull(fieldNamesForExtraction, "fieldNamesForExtraction");
            FieldNamesForExtraction = fieldNamesForExtraction;
        }

        public AdaptedResultList Excise(AdaptedResultList fields)
        {
            if(fields == null || fields.Count() < 1)
            {
                return fields;
            }

            List<AdaptedControlResult> adaptedControlResults = new List<AdaptedControlResult>();

            foreach(AdaptedControlResult field in fields)
            {
                adaptedControlResults.Add(GetExtractValueFieldIfApplicable(field));
            }

            return adaptedControlResults;
        }

        private AdaptedControlResult GetExtractValueFieldIfApplicable(AdaptedControlResult field)
        {
            if(ShouldExtractFieldValue(field))
            {
                return GetExtractedValueField(field);
            }

            return field;
        }

        private bool ShouldExtractFieldValue(ControlResult field)
        {
            return FieldNamesForExtraction.Contains(field.FieldName);
        }

        private static AdaptedControlResult GetExtractedValueField(ControlResult field)
        {
            return new AdaptedControlResult(CreateNewControlResultWithEmptyValue(field), true);
        }

        private static ControlResult CreateNewControlResultWithEmptyValue(ControlResult field)
        {
            ControlResult controlResult = new ControlResult(field.FieldName, string.Empty, field.Parameters);
            controlResult.FieldID = field.FieldID;
            return controlResult;
        }

        public static IWFFMFieldValuesExcisor CreateNewWFFMFieldValuesExcisor(IEnumerable<string> fieldNamesForExtraction)
        {
            return new WFFMFieldValuesExcisor(fieldNamesForExtraction);
        }
    }
}

For scalability purposes and to avoid hard-coding field name values, I put my targeted fields — I only have one at the moment — into a config file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<settings>
			<setting name="ExciseFields.SocialSecurityNumberField" value="Social Security Number" />
		</settings>
	</sitecore>
</configuration>

I then created a custom save to database action where I call my utility class to rip out my specified targeted fields, and pass that through onto the base save to database action class:

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

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Form.Core.Client.Data.Submit;
using Sitecore.Form.Submit;

using Sitecore.Sandbox.Utilities.Excisors;
using Sitecore.Sandbox.Utilities.Excisors.Base;

namespace Sitecore.Sandbox.WFFM.Actions
{
    public class ExciseFieldValuesThenSaveToDatabase : SaveToDatabase
    {
        private static readonly IEnumerable<string> FieldsToExcise = new string[] { Settings.GetSetting("ExciseFields.SocialSecurityNumberField") };
        
        public override void Execute(ID formId, AdaptedResultList fields, object[] data)
        {
            base.Execute(formId, ExciseFields(fields), data);
        }

        private AdaptedResultList ExciseFields(AdaptedResultList fields)
        {
            IWFFMFieldValuesExcisor excisor = WFFMFieldValuesExcisor.CreateNewWFFMFieldValuesExcisor(FieldsToExcise);
            return excisor.Excise(fields);
        }
    }
}

I created a new item to register my custom save to database action under /sitecore/system/Modules/Web Forms for Marketers/Settings/Actions/Save Actions:
registered-save-db-action

I then created my form:

built-irs-form

I set this form on a new page item, and published all of my changes above.

Now, it’s time to test this out. I navigated to my form, and filled it in as an irate tax payer might:

filled-in-irs-form

Looking at my form’s report, I see that the social security number was blanked out before being saved to the database:

irs-form-report

A similar solution could also be used to add new field values into the collection of fields — values of fields that wouldn’t be available on the form, but should be displayed in the form report. An example might include a transaction ID or approval ID returned from a third-party credit card processor. Instead of ripping values, values would be inserted into the collection of fields, and then passed along to the base save to database action class.