Home » Web Forms for Marketers (Page 2)

Category Archives: Web Forms for Marketers

Specify Which Sitecore Web Forms for Marketers Form To Render Via the Query String

Today I saw a post in one of the SDN forums asking how one could go about building a page in Sitecore that can render a Web Forms for Marketers (WFFM) form based on an ID passed via the query string.

I built the following WFFM FormRenderer as a “proof of concept” to accomplish this — this solution assumes the ID we are passing is the ID of the form, and not some other ID (or guid):

using System;
using System.Web;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Form.Core.Configuration;
using Sitecore.Form.Core.Renderings;

namespace Sitecore.Sandbox.Form.Core.Renderings
{
    public class DetectIDFormRenderer : FormRender
    {
        protected override void OnInit(System.EventArgs e)
        {
            string detectedFormId = GetDetectedFormId();
            if (IsValidFormId(detectedFormId))
            {
                FormID = detectedFormId;
            }
            
            base.OnInit(e);
        }

        private static string GetDetectedFormId()
        {
            return HttpContext.Current.Request["formId"];
        }

        private static bool IsValidFormId(string id)
        {
            return !string.IsNullOrWhiteSpace(id) 
                    && IsID(id) 
                    && IsFormId(id);
        }
        
        private static bool IsID(string id)
        {
            Sitecore.Data.ID sitecoreID;
            return Sitecore.Data.ID.TryParse(id, out sitecoreID);
        }

        private static bool IsFormId(string id)
        {
            Item item = StaticSettings.ContextDatabase.GetItem(id);
            return item != null && item.TemplateID == IDs.FormTemplateID;
        }
    }
}

The FormRenderer above grabs the specified form’s ID via a query string parameter, ascertains whether it’s truly an ID, and determines whether it is an ID of a WFFM Form in Sitecore — these are done via the IsID and IsFormId methods.

If the supplied form ID is valid, we save it to the FormID property defined in the base FormerRender class. Otherwise, we flow through to the “out of the box” logic.

Now it’s time to register the above class in Sitecore.

I duplicated the “out of the box” Form Webcontrol under /sitecore/layout/Renderings/Modules/Web Forms for Marketers, renamed the item to something appropriate, and updated the code-defining fields to point to our new FormRender above:

Detect-ID-Form-FormRenderer

I decided to reuse an existing page item with a WFFM form — I didn’t want to step through ‘Insert Form’ wizard so that I could save time — and swapped out the “out of the box” Form Webcontrol with the new one we created above:

webcontrol-switch

I ensured we had a default form set just in case of query string manipulation, or in the event the form cannot be found by the given ID:

default-form-is-set

I published everything, and navigated to my form page:

no-form-specified

I then specified the empty guid:

invalid-form-specified

I manipulated the query string again, but this time passing a valid form ID:

valid-form-specified

I then changed the form ID again but with another valid form ID:

another-valid-form-specified

If you have any suggestions around making this better, or ideas for a different solution, please drop a comment.

Add JavaScript to the Client OnClick Event of the Sitecore WFFM Submit Button

A SDN forum thread popped up a week and a half ago asking whether it were possible to attach a Google Analytics event to the WFFM submit button — such would involve adding a snippet of JavaScript to the OnClick attribute of the WFFM submit button’s HTML — and I was immediately curious how one would go about achieving this, and whether this were possible at all.

I did a couple of hours of research last night — I experimented with custom processors of pipelines used by WFFM — but found no clean way of adding JavaScript to the OnClick event of the WFFM submit button.

However — right before I was about to throw in the towel for the night — I did find a solution on how one could achieve this — albeit not necessarily a clean solution since it involves some HTML manipulation (I would opine using the OnClientClick attribute of an ASP.NET Button to be cleaner, but couldn’t access the WFFM submit button due to its encapsulation and protection level in a WFFM WebControl) — via a custom Sitecore.Form.Core.Renderings.FormRender:

using System.IO;
using System.Linq;
using System.Web.UI;

using Sitecore.Form.Core.Renderings;

using HtmlAgilityPack;

namespace Sitecore.Sandbox.Form.Core.Renderings
{
    public class AddOnClientClickFormRender : FormRender
    {
        private const string ConfirmJavaScriptFormat = "if(!confirm('Are you sure you want to submit this form?')) {{ return false; }} {0} ";

        protected override void DoRender(HtmlTextWriter output)
        {
            string html = string.Empty;
            using (StringWriter stringWriter = new StringWriter())
            {
                using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(stringWriter))
                {
                    base.DoRender(htmlTextWriter);
                }

                html = AddOnClientClickToSubmitButton(stringWriter.ToString());
            }

            output.Write(html);
        }

        private static string AddOnClientClickToSubmitButton(string html)
        {
            if (string.IsNullOrWhiteSpace(html))
            {
                return html;
            }

            HtmlNode submitButton = GetSubmitButton(html);
            if (submitButton == null && submitButton.Attributes["onclick"] != null)
            {
                return html;
            }

            submitButton.Attributes["onclick"].Value = string.Format(ConfirmJavaScriptFormat, submitButton.Attributes["onclick"].Value);
            return submitButton.OwnerDocument.DocumentNode.InnerHtml;
        }

        private static HtmlNode GetSubmitButton(string html)
        {
            HtmlNode documentNode = GetHtmlDocumentNode(html);
            return documentNode.SelectNodes("//input[@type='submit']").FirstOrDefault();
        }

        private static HtmlNode GetHtmlDocumentNode(string html)
        {
            HtmlDocument htmlDocument = CreateNewHtmlDocument(html);
            return htmlDocument.DocumentNode;
        }

        private static HtmlDocument CreateNewHtmlDocument(string html)
        {
            HtmlDocument htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(html);
            return htmlDocument;
        }
    }
}

The FormRender above uses Html Agility Pack — which comes with Sitecore — to retrieve the submit button in the HTML that is constructed by the base FormRender class, and adds a snippet of JavaScript to the beginning of the OnClick attribute (there is already JavaScript in this attribute, and we want to run our JavaScript first).

I didn’t wire up a Google Analytics event to the submit button in this FormRender — it would’ve required me to spin up an account for my local sandbox instance, and I feel this would’ve been overkill for this post.

Instead — as an example of adding JavaScript to the OnClick attribute of the WFFM submit button — I added code to launch a JavaScript confirmation dialog asking the form submitter whether he/she would like to continue submitting the form. If the user clicks the ‘Cancel’ button, the form is not submitted, and is submitted if the user clicks ‘OK’.

I then had to hook this custom FormRender to the WFFM Form Rendering — /sitecore/layout/Renderings/Modules/Web Forms for Marketers/Form — in Sitecore:

form-rendering

I then saved, published, and navigated to a WFFM test form. I then clicked the submit button:

confirmation-box

As you can see, I was prompted with a JavaScript confirmation dialog box.

If you have any thoughts on this implementation, or know of a better way to do this, please drop a comment.

Until next time, have a Sitecorelicious day! ­čÖé

Encrypt Web Forms For Marketers Fields in Sitecore

In an earlier post, I walked you through how I experimented with data encryption of field values in Sitecore, and alluded to how I had done a similar thing for the Web Forms For Marketers (WFFM) module on a past project at work.

Months have gone by, and guilt has begun to gnaw away at my entire being — no, not really, I’m exaggerating a bit — but I definitely have been feeling bad for not sharing a solution.

In order to shake feeling bad, I decided to put my nose to the grindstone over the past few days to come up with a different solution than the one I had built at work, and this post shows the fruits of that labor.

I decided to reuse the interface I had created in my older post on data encryption. I am re-posting it here for reference:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace Sitecore.Sandbox.Security.Encryption.Base
{
    public interface IEncryptor
    {
        string Encrypt(string input);
 
        string Decrypt(string input);
    }
}

I then asked myself “What encryption algorithm should I use?” I scavenged through the System.Security.Cryptography namespace in mscorlib.dll using .NET Reflector, and discovered some classes, when used together, achieve data encryption using the RC2 algorithm — an algorithm I know nothing about:

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

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Security.Encryption.Base;

namespace Sitecore.Sandbox.Security.Encryption
{
    public class RC2Encryptor : IEncryptor
    {
        public string Key { get; set; }

        private RC2Encryptor(string key)
        {
            SetKey(key);
        }

        private void SetKey(string key)
        {
            Assert.ArgumentNotNullOrEmpty(key, "key");
            Key = key;
        }

        public string Encrypt(string input)
        {
            return Encrypt(input, Key);
        }

        public static string Encrypt(string input, string key)
        {
            byte[] inputArray = UTF8Encoding.UTF8.GetBytes(input);
            RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();
            rc2.Key = UTF8Encoding.UTF8.GetBytes(key);
            rc2.Mode = CipherMode.ECB;
            rc2.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = rc2.CreateEncryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
            rc2.Clear();
            return System.Convert.ToBase64String(resultArray, 0, resultArray.Length);
        }

        public string Decrypt(string input)
        {
            return Decrypt(input, Key);
        }

        public static string Decrypt(string input, string key)
        {
            byte[] inputArray = System.Convert.FromBase64String(input);
            RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();
            rc2.Key = UTF8Encoding.UTF8.GetBytes(key);
            rc2.Mode = CipherMode.ECB;
            rc2.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = rc2.CreateDecryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
            rc2.Clear();
            return UTF8Encoding.UTF8.GetString(resultArray);
        }

        public static IEncryptor CreateNewRC2Encryptor(string key)
        {
            return new RC2Encryptor(key);
        }
    }
}

As I had mentioned in my previous post on data encryption, I am not a cryptography expert, nor a security expert.

I am not aware of how strong the RC2 encryption algorithm is, or what it would take to crack it. I strongly advise against using this algorithm in any production system without first consulting with a security expert. I am using it in this post only as an example.

If you happen to be a security expert, or are able to compare encryption algorithms defined in the System.Security.Cryptography namespace in mscorlib.dll, please share in a comment.

In a previous post on manipulating field values for WFFM forms, I had to define a new class that implements Sitecore.Forms.Data.IField in Sitecore.Forms.Core.dll in order to change field values — it appears the property mutator for the “out of the box” class is ignored — and decided to reuse it here:

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 created a WFFM Data Provider that encrypts and decrypts field names and values:

using System;
using System.Collections.Generic;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Forms.Data;
using Sitecore.Forms.Data.DataProviders;
using Sitecore.Reflection;

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

namespace Sitecore.Sandbox.WFFM.Forms.Data.DataProviders
{
    public class WFFMEncryptionDataProvider : WFMDataProviderBase
    {
        private WFMDataProviderBase InnerProvider { get; set; }
        private IEncryptor Encryptor { get; set; }

        public WFFMEncryptionDataProvider(string innerProvider)
            : this(CreateInnerProvider(innerProvider), CreateDefaultEncryptor())
        {
        }

        public WFFMEncryptionDataProvider(string connectionString, string innerProvider)
            : this(CreateInnerProvider(innerProvider, connectionString), CreateDefaultEncryptor())
        {
        }

        public WFFMEncryptionDataProvider(WFMDataProviderBase innerProvider)
            : this(innerProvider, CreateDefaultEncryptor())
        {
        }

        public WFFMEncryptionDataProvider(WFMDataProviderBase innerProvider, IEncryptor encryptor)
        {
            SetInnerProvider(innerProvider);
            SetEncryptor(encryptor);
        }

        private static WFMDataProviderBase CreateInnerProvider(string innerProvider, string connectionString = null)
        {
            Assert.ArgumentNotNullOrEmpty(innerProvider, "innerProvider");
            if (!string.IsNullOrWhiteSpace(connectionString))
            {
                return ReflectionUtil.CreateObject(innerProvider, new[] { connectionString }) as WFMDataProviderBase;
            }

            return ReflectionUtil.CreateObject(innerProvider, new object[0]) as WFMDataProviderBase;
        }
        
        private void SetInnerProvider(WFMDataProviderBase innerProvider)
        {
            Assert.ArgumentNotNull(innerProvider, "innerProvider");
            InnerProvider = innerProvider;
        }

        private static IEncryptor CreateDefaultEncryptor()
        {
            return DataNullTerminatorEncryptor.CreateNewDataNullTerminatorEncryptor(GetEncryptorSettings());
        }

        private static DataNullTerminatorEncryptorSettings GetEncryptorSettings()
        {
            return new DataNullTerminatorEncryptorSettings
            {
                EncryptionDataNullTerminator = Settings.GetSetting("WFFM.Encryption.DataNullTerminator"),
                InnerEncryptor = RC2Encryptor.CreateNewRC2Encryptor(Settings.GetSetting("WFFM.Encryption.Key"))
            };
        }

        private void SetEncryptor(IEncryptor encryptor)
        {
            Assert.ArgumentNotNull(encryptor, "encryptor");
            Encryptor = encryptor;
        }

        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)
        {
            IEnumerable<IForm> forms = InnerProvider.GetForms(queryParams, out total);
            DecryptForms(forms);
            return forms;
        }

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

        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)
        {
            EncryptForm(form);
            InnerProvider.InsertForm(form);
        }

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

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

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

        private void EncryptForms(IEnumerable<IForm> forms)
        {
            Assert.ArgumentNotNull(forms, "forms");
            foreach (IForm form in forms)
            {
                EncryptForm(form);
            }
        }

        private void EncryptForm(IForm form)
        {
            Assert.ArgumentNotNull(form, "form");
            Assert.ArgumentNotNull(form.Field, "form.Field");
            form.Field = EncryptFields(form.Field);
        }

        private IEnumerable<IField> EncryptFields(IEnumerable<IField> fields)
        {
            Assert.ArgumentNotNull(fields, "fields");
            IList<IField> encryptedFields = new List<IField>();
            foreach (IField field in fields)
            {
                encryptedFields.Add(EncryptField(field));
            }

            return encryptedFields;
        }

        private IField EncryptField(IField field)
        {
            Assert.ArgumentNotNull(field, "field");
            return CreateNewWFFMField(field, Encrypt(field.FieldName), Encrypt(field.Value));
        }

        private void DecryptForms(IEnumerable<IForm> forms)
        {
            Assert.ArgumentNotNull(forms, "forms");
            foreach (IForm form in forms)
            {
                DecryptForm(form);
            }
        }

        private void DecryptForm(IForm form)
        {
            Assert.ArgumentNotNull(form, "form");
            Assert.ArgumentNotNull(form.Field, "form.Field");
            form.Field = DecryptFields(form.Field);
        }

        private IEnumerable<IField> DecryptFields(IEnumerable<IField> fields)
        {
            Assert.ArgumentNotNull(fields, "fields");
            IList<IField> decryptedFields = new List<IField>();
            foreach (IField field in fields)
            {
                decryptedFields.Add(DecryptField(field));
            }

            return decryptedFields;
        }

        private IField DecryptField(IField field)
        {
            Assert.ArgumentNotNull(field, "field");
            return CreateNewWFFMField(field, Decrypt(field.FieldName), Decrypt(field.Value));
        }

        private string Encrypt(string input)
        {
            return Encryptor.Encrypt(input);
        }

        private string Decrypt(string input)
        {
            return Encryptor.Decrypt(input);
        }

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

            return null;
        }
    }
}

The above class employs the decorator pattern. An inner WFFM Data Provider — which is supplied via a parameter configuration node in \App_Config\Include\forms.config, and is created via magic within the Sitecore.Reflection.ReflectionUtil class — is wrapped.

Methods that save and retrieve form data in the above Data Provider decorate the same methods defined on the inner WFFM Data Provider.

Methods that save form data pass form(s) — and eventually their fields — through a chain of Encrypt methods. The Encrypt method that takes in an IField instance as an argument encrypts the instance’s field name and value, and returns a new instance of the WFFMField class using the encrypted data and the other properties on the IField instance untouched.

Similarly, a chain of Decrypt methods are called for form(s) being retrieved from the inner Data Provider — field names and values are decrypted and saved into a new instance of the WFFMField class, and the manipulated form(s) are returned.

I want to point out that the IEncryptor instance is actually an instance of DataNullTerminatorEncryptor — see my earlier post on data encryption to see how this is implemented — which decorates our RC2Encryptor. This decorating encryptor stamps encrypted strings with a special string so we don’t accidentally encrypt a value twice, and it also won’t try to decrypt a string value that isn’t encrypted.

I added a new include configuration file to hold encryption related settings — the IEncryptor’s key, and the string that will be put at the end of all encrypted data via the DataNullTerminatorEncryptor instance:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <!-- TODO: change the terminator so it does not scream "PLEASE TRY TO CRACK ME!" -->
      <setting name="WFFM.Encryption.DataNullTerminator" value="#I_AM_ENCRYPTED#" />

      <!-- I found this key somewhere on the internet, so it must be secure -->
      <setting name="WFFM.Encryption.Key" value="88bca90e90875a" />
    </settings>
  </sitecore>
</configuration>

I then hooked in the encryption WFFM Data Provider in \App_Config\Include\forms.config, and set the type for the inner provider:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
  
	<!-- There is stuff up here -->
  
	<!-- MS SQL -->
	<formsDataProvider type="Sitecore.Sandbox.WFFM.Forms.Data.DataProviders.WFFMEncryptionDataProvider, Sitecore.Sandbox">
		<!-- No, this is not my real connection string -->
		<param desc="connection string">user id=(user);password=(password);Data Source=(database)</param>
		<param desc="inner provider">Sitecore.Forms.Data.DataProviders.WFMDataProvider, Sitecore.Forms.Core</param>
	</formsDataProvider>

	<!-- There is stuff down here -->
	
	</sitecore>
</configuration>

Let’s see this in action.

I created a new WFFM form with some fields for testing:

the-form-in-sitecore

I then mapped the above form to a new page in Sitecore, and published both the form and page.

I navigated to the form page, and filled it out:

the-form-page

After clicking submit, I was given a ‘thank you’ page’:

the-form-page-confirmation

Let’s see what our field data looks like in the WFFM database:

the-form-submission-encrypted-db

As you can see, the data is encrypted.

Now, let’s see if the data is also encrypted in the Forms Report for our test form:

forms-report-encryption

As you can see, the end-user would be unaware that any data manipulation is happening under the hood.

If you have any thoughts on this, please leave a comment.

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

In a previous article, I discussed how one could remove Web Forms for Marketers (WFFM) field values before saving form data into the WFFM database — which ultimately make their way into the Form Reports for your form.

When I penned that article, my intentions were not to write an another highlighting the polar opposite action of inserting field values — I figured one could easily ascertain how to do this from that article.

However, last night I started to feel some guilt for not sharing how one could do this — there is one step in this process that isn’t completely intuitive — so I worked late into the night developing a solution of how one would go about doing this, and this article is the fruit of that effort.

Besides, how often does one get the opportunity to use the word shoehorn? If you aren’t familiar with what a shoehorn is, check out this article. I am using the word as an action verb in this post — meaning to insert values for fields in the WFFM database. ­čÖé

I first defined a utility class and its interfaces for shoehorning field values into a AdaptedResultList collection — a collection of WFFM fields:

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

namespace Sitecore.Sandbox.Utilities.Shoehorns.Base
{
    public interface IShoehorn<T, U>
    {
        U Shoehorn(T source);
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

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

using Sitecore.Sandbox.Utilities.Shoehorns.Base;

namespace Sitecore.Sandbox.Utilities.Shoehorns.Base
{
    public interface IWFFMFieldValuesShoehorn : IShoehorn<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.Shoehorns.Base;

namespace Sitecore.Sandbox.Utilities.Shoehorns
{
    public class WFFMFieldValuesShoehorn : IWFFMFieldValuesShoehorn
    {
        private IDictionary<string, string> _FieldsForShoehorning;
        private IDictionary<string, string> FieldsForShoehorning 
        { 
            get
            {
                if (_FieldsForShoehorning == null)
                {
                    _FieldsForShoehorning = new Dictionary<string, string>();
                }

                return _FieldsForShoehorning;
            }
        }

        private WFFMFieldValuesShoehorn(IEnumerable<KeyValuePair<string, string>> fieldsForShoehorning)
        {
            SetFieldsForShoehorning(fieldsForShoehorning);
        }

        private void SetFieldsForShoehorning(IEnumerable<KeyValuePair<string, string>> fieldsForShoehorning)
        {
            AssertFieldsForShoehorning(fieldsForShoehorning);
            
            foreach (var keyValuePair in fieldsForShoehorning)
            {
                AddToDictionaryIfPossible(keyValuePair);
            }
        }

        private void AddToDictionaryIfPossible(KeyValuePair<string, string> keyValuePair)
        {
            if (!FieldsForShoehorning.ContainsKey(keyValuePair.Key))
            {
                FieldsForShoehorning.Add(keyValuePair.Key, keyValuePair.Value);
            }
        }

        private static void AssertFieldsForShoehorning(IEnumerable<KeyValuePair<string, string>> fieldsForShoehorning)
        {
            Assert.ArgumentNotNull(fieldsForShoehorning, "fieldsForShoehorning");

            foreach (var keyValuePair in fieldsForShoehorning)
            {
                Assert.ArgumentNotNullOrEmpty(keyValuePair.Key, "keyValuePair.Key");
            }
        }

        public AdaptedResultList Shoehorn(AdaptedResultList fields)
        {
            if (!CanProcessFields(fields))
            {
                return fields;
            }

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

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

            return adaptedControlResults;
        }

        private static bool CanProcessFields(IEnumerable<ControlResult> fields)
        {
            return fields != null && fields.Any();
        }

        private AdaptedControlResult GetShoehornedField(AdaptedControlResult field)
        {
            string value = string.Empty;

            if (FieldsForShoehorning.TryGetValue(field.FieldName, out value))
            {
                return GetShoehornedField(field, value);
            }

            return field;
        }

        private static AdaptedControlResult GetShoehornedField(ControlResult field, string value)
        {
            return new AdaptedControlResult(CreateNewControlResultWithValue(field, value), true);
        }

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

        public static IWFFMFieldValuesShoehorn CreateNewWFFMFieldValuesShoehorn(IEnumerable<KeyValuePair<string, string>> fieldsForShoehorning)
        {
            return new WFFMFieldValuesShoehorn(fieldsForShoehorning);
        }
    }
}

This class is similar to the utility class I’ve used in my article on ripping out field values, albeit we are inserting values that don’t necessarily have to be the empty string.

I then defined a new WFFM field type — an invisible field that serves as a placeholder in our Form Reports for a given form. This is the non-intuitive step I was referring to above:

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.Diagnostics;

using Sitecore.Form.Web.UI.Controls;

namespace Sitecore.Sandbox.WFFM.Controls
{
    public class InvisibleField : SingleLineText
    {
        public InvisibleField()
        {
        }

        public InvisibleField(HtmlTextWriterTag tag) 
            : base(tag)
        {
        }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            HideControls(new Control[] { title, generalPanel });
        }

        private static void HideControls(IEnumerable<Control> controls)
        {
            Assert.ArgumentNotNull(controls, "controls");

            foreach (Control control in controls)
            {
                HideControl(control);
            }
        }

        private static void HideControl(Control control)
        {
            Assert.ArgumentNotNull(control, "control");
            control.Visible = false;
        }
    }
}

I had to register this new field in WFFM in the Sitecore Client under /sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types/Custom:

invisible-field

Next, I created a custom field action that processes WFFM fields — and shoehorns values into fields where appropriate.

In my example, I will be capturing users’ browser user agent strings and their ASP.NET session identifiers — I strongly recommend defining your field names in a patch config file, although I did not do such a step for this post:

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

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

using Sitecore.Sandbox.Utilities.Shoehorns;
using Sitecore.Sandbox.Utilities.Shoehorns.Base;

namespace Sitecore.Sandbox.WFFM.Actions
{
    public class ShoehornFieldValuesThenSaveToDatabase : SaveToDatabase
    {
        public override void Execute(ID formId, AdaptedResultList fields, object[] data)
        {
            base.Execute(formId, ShoehornFields(fields), data);
        }

        private AdaptedResultList ShoehornFields(AdaptedResultList fields)
        {
            IWFFMFieldValuesShoehorn shoehorn = WFFMFieldValuesShoehorn.CreateNewWFFMFieldValuesShoehorn(CreateNewFieldsForShoehorning());
            return shoehorn.Shoehorn(fields);
        }

        private IEnumerable<KeyValuePair<string, string>> CreateNewFieldsForShoehorning()
        {
            return new KeyValuePair<string, string>[] 
            { 
                new KeyValuePair<string, string>("User Agent", HttpContext.Current.Request.UserAgent),
                new KeyValuePair<string, string>("Session ID", HttpContext.Current.Session.SessionID)
            };
        }
    }
}

I then registered my new Save to Database action in Sitecore under /sitecore/system/Modules/Web Forms for Marketers/Settings/Actions/Save Actions:

shoehorn-save-to-db

Let’s build a form. I created another random form. This one contains some invisible fields:

another-random-form

Next, I mapped my custom Save to Database action to my new form:

another-random-form-add-save-to-db-action

I also created a new page item to hold my WFFM form, and navigated to that page to fill in my form:

another-random-form-filled

I clicked the submit button and got a pleasant confirmation message:

another-random-form-confirmation

Thereafter, I pulled up the Form Reports for this form, and see that field values were shoehorned into it:

another-random-form-reports

That’s all there is to it.

What is the utility in all of this? Well, you might want to use this approach when sending information off to a third-party via a web service where that third-party application returns some kind of transaction identifier. Having this identifier in your Form Reports could help in troubleshoot issues that had arisen during that transaction — third-party payment processors come to mind.

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. ­čÖé

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.