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.

Tweets

Advertisements

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.

Advertisements

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 )

Google+ photo

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

Twitter picture

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

Facebook photo

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

w

Connecting to %s

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

%d bloggers like this: