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

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

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

Enter your email address to follow this blog and receive notifications of new posts by email.

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.

Advertisement

5 Comments

  1. […] a previous article, I discussed how one could remove Web Forms for Marketers (WFFM) field values before saving form […]

  2. Jeff says:

    Very interesting post! It seems there isn’t that much WFFM documentation out there. This is good to know in the event that I ever use WFFM to collect payment information. I’ll have to bookmark your site!

    On a somewhat related note – do you know if WFFM offers an opt-out option, so that people who have already submitted a form can remove themselves from the related database as well? All ideas welcome.

  3. Great Post on WFFM.I plan to create a form with countries > states populated, so seeing how custom fields are created really helps me out!

  4. Shailesh says:

    I used this ref. example but I am not able to see the ” Social security field ” in The form .

Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: