Hide menu Last updated: Oct 05 2017
Area: Episerver Add-ons Applies to versions: 2 and higher

How cookies work

This topic is about how Episerver Forms deals with cookies to keep track of end-user identifiers as well as the status of form submissions. 

List of cookies in Episerver Forms

Like other web applications, Episerver Forms uses cookies to implement some business rules and to provide visitors with a better experience. The following cookies are used in Episerver Forms.

  •  .EPiForm_BID. This cookie is for distinguishing one browser from other browsers that a visitor is using while surfing the internet. If a user visits an Episerver site for the first time, Episerver Forms automatically assigns a random GUID to the visitor's browser. The GUID is stored in a cookie and when it has expired, Episerver Forms creates a new one the next time the user visits the website.
  • .EPiForm_VisitorIdentifier. This cookie is used to identify who is interacting with the application. It is created by combining the value of .EPiForm_BID with the current user name in case the user is already logged in. A colon is used as a separator. If the user is not logged in, the value of .EPiForm_VisitorIdentifier is the value of .EPiForm_BID concatenated with a colon. The sample value of this cookie is ‘0f8fad5b-d9cb-469f-a165-70867728950e:admin’.
  • In order to keep track of which forms a user is interacting with and if they are either completed or uncompleted, Episerver Forms uses a cookie name progressive cookie. The format of the cookie key is 'EPiForm_{FormGUID}_{VisitorIdentifier}'. FormGUID is the primary key of a form instance. Each form instance is given a GUID as unique identifier right after it is created and VisitorIdentifier is the value of the cookie .EPiForm_VisitorIdentifier mentioned above. A sample progressive cookie key is
    EPiForm_1g9kfad5z-d9bb-964f-a165-7086772895e_0f8fad5b-d9cb-469f-a165-70867728950e:admin

The value of the progressive cookie is a JSON object consisting of following properties: 

  • FormGUID. Already mentioned earlier.
  • Submission ID. The ID of the stored submission data (both finalized and unfinalized) associated with the FormGUID.
  • IsFinalized. The status of the form submission. The value is true if the user finalizes the form by either clicking the Finalize Submit button or finishing all steps of a form instance.  The Submit button form element has a property name Finalized. If, for example, a form has three steps and you want to provide users with an opportunity to finish the form at the second step, you can drag a submit button into the form and set the value that property to true. Now users can decide to go through all three steps or finish the form at second step.

Finalize property of submit button

Process for handling cookies

After submitting a form or navigating to another form step, Episerver Forms checks if the progressive cookie related to the form instance (and current VisitorIdentifier) exists or not:

  1. Progressive cookie exists
    If the cookie does exist, the server retrieves the IsFinalized value from that cookie. If the value is true, Episerver Forms gets the Form Submission ID from the submitted data (via a hidden field); otherwise, the  Submission ID is obtained from the progressive cookie. If the value of IsFinalized is true, it means that the form allows multiple submissions from the same IP address/cookie and that the user has completed submitting data at least once plus this time of submitting.
  2. Progressive cookie does not exist
    If the progressive cookie does not exist, Episerver Forms gets the Submission ID from the submitted data. Next, Episerver Forms checks if the Submission ID is null. If it is null, the submission data is saved to the storage as a new record. If it is not null, it means that the form instance has multi-steps and the user has already interacted with that form but not completed whole steps. The submitted data is updated to an existing record by the Submission ID.

After saving or updating submission data, the output of the process is SubmissionID and progressive cookie is set (creating a new one if noone exists or updating existing cookie). The last process is to load the form elements of the next step along with the Submission ID and display on the page. The Submission ID is used in a hidden field for the next navigation or submission.

Because an Episerver Forms application may have multiple instances of a form (for example, Form_1 and Form_2), there may be many progressive cookies. For example, if a user visits an Episerver site and just finishes step 1 out of three steps on Form_1, and then directly jumps to Form_2 and starts entering information, then two progressive cookies are created. Those cookies tell the server that the user interacted with two different form instances, no matter if they were completed or not.

Episerver Forms process of handling cookie

 

JavaScript enabled

If Javascript is enabled, navigating between steps (both previous step and next step) are performed via an AJAX request and the client uses the session storage to temporarily store submission data. Thus, if the form has multiple steps and a user fills in form data in the last step but wants to go back to the previous step, the entered data is still being retained. The user does not have to re-insert data and the server does not need to hit the database to fetch data.

JavaScript disabled

If Javascript is disabled, navigating between steps causes the browser to get redirected to a certain page to display form steps and Episerver has to hit the database to retrieve already entered data, and fill in form elements.

Managing cookie expiration time

For consistency, all cookies mentioned above are set to the same expiration time.
The IVisitorIdentifyProvider interface is responsible for handling form cookie expiration time of visitors. DefaultVisitorIdentifyProvider is the default implementation. There are few points to note:

  • The function SetVisitorIdentifier is used to set the .EPiForm_VisitorIdentifier cookie for identifying visitors.
  • Currently, the expiration time unit is based on day and the default expiration time is 90 days. You can modify this setting in the Forms.config file with the key visitorSubmitTimeout.

  • The property Order: Episerver Forms scans all assemblies to find out which implementation of IVisitorIdentifyProvider interface will be executed based on Ascending Order. If there are multiple implementations, the implementation with the lowest order is executed. The default order is 1000 in the DefaultVisitorIdentifyProvider class.
  • If you want to customize the expiration time of Episerver Forms, you need to implement the IVisitorIdentifyProvider interface either by inheriting the DefaultVisitorIdentifyProvider class and overriding desired functions or creating a new class and implementing all functions in IVisitorIdentifyProvider from scratch. The recommendation is to inherit the DefaultVisitorIdentifyProvider class for simplicity. Example code:
using EPiServer.Forms.Core;
using EPiServer.Forms.Core.Internal;
using EPiServer.Forms.Core.Internal.VisitorIdentify;
using EPiServer.Logging;
using EPiServer.ServiceLocation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using EPiServer.Forms.Core.Models.Internal;

namespace EpiserverSiteLM.CustomForms
{

    /// <summary>
    /// Handling form cookie expiration time.
    /// </summary>
    [ServiceConfiguration(typeof(IVisitorIdentifyProvider))]
    public class CustomVisitorIdentifyProvider : DefaultVisitorIdentifyProvider
    {
        private static readonly object _lock = new object();
        private static readonly ILogger _logger = LogManager.GetLogger(typeof(VisitorIdentifyService));
        private const int _expirationTimeInMinute = 20;

        /// <summary>
        /// Episerver Container will scan in all assemblies to find out which Implementation will get executed.
        /// If there are multi-implementations, class with lowest Order will be executed. The Order of DefaultVisitorIdentifyProvider class is 1000.
        /// So we have to set this value to 1 (as long as lower than 1000)
        /// </summary>
        public override int Order { get { return 1; } }

        /// <summary>
        /// Set form cookie. We keep everything as default in base class except modifying the expiration time.
        /// </summary>
        /// <param name="visitorIdentifier"></param>
        public override void SetVisitorIdentifier(string visitorIdentifier)
        {
            // TECH NOTE: When access cookies from multi threads, sometime it throw exceptions even we use lock for synchronizing.
            // So that we need surround code with try/catch to make sure the exception does not break the request.

            HttpCookie cookieBrowserID;
            HttpCookie cookieVI;

            lock (_lock)
            {
                try
                {
                    cookieBrowserID = Context.Request.Cookies[CookieName_Forms_BrowserID] ?? new HttpCookie(CookieName_Forms_BrowserID);
                }
                catch (Exception)
                {
                    _logger.Warning($"Cannot get cookies: {CookieName_Forms_BrowserID}");
                    cookieBrowserID = new HttpCookie(CookieName_Forms_BrowserID);
                }

                try
                {
                    cookieVI = Context.Request.Cookies[CookieName_Forms_VisitorIdentifier] ?? new HttpCookie(CookieName_Forms_VisitorIdentifier);
                }
                catch (Exception)
                {
                    _logger.Warning($"Cannot get cookies: {CookieName_Forms_VisitorIdentifier}");
                    cookieVI = new HttpCookie(CookieName_Forms_VisitorIdentifier);
                }
            }

            string browserID = string.Empty,
                newVI = string.Empty;

            if (string.IsNullOrWhiteSpace(visitorIdentifier))
            {
                browserID = GenerateBrowserId();
                newVI = BuildVisitorIdentifier(browserID, GetCurrentUserID());
            }
            else
            {
                var arrSplit = visitorIdentifier.Split(new string[] { SEPARATOR }, StringSplitOptions.RemoveEmptyEntries);
                browserID = arrSplit[0];
                newVI = visitorIdentifier;
            }

            cookieBrowserID.Value = browserID;

            // The default time unit is based on day. 
            //cookieBrowserID.Expires = DateTime.Now.AddDays(_formConfig.Service.VisitorSubmitTimeout);

            // If you want to use minute as unit then use this code
            cookieBrowserID.Expires = DateTime.Now.AddMinutes(_expirationTimeInMinute); // will expire in 20 minutes

            cookieBrowserID.Path = "/";

            cookieVI.Value = newVI;
            // The default time unit is based on day. 
            //cookieVI.Expires = DateTime.Now.AddDays(_formConfig.Service.VisitorSubmitTimeout);

            // If you want to use minute as unit then use this code
            cookieVI.Expires = DateTime.Now.AddMinutes(_expirationTimeInMinute); // will expire in 20 minutes

            cookieVI.Path = "/";

            lock (_lock)
            {
                try
                {
                    if (Context.Response.Cookies.Keys.OfType<string>().Contains(cookieBrowserID.Name))
                    {
                        Context.Response.Cookies.Set(cookieBrowserID);
                    }
                    else
                    {
                        Context.Response.Cookies.Add(cookieBrowserID);
                    }
                }
                catch (Exception)
                {
                    _logger.Warning($"Cannot set cookies: {cookieBrowserID.Name}");
                }

                try
                {
                    if (Context.Response.Cookies.Keys.OfType<string>().Contains(cookieVI.Name))
                    {
                        Context.Response.Cookies.Set(cookieVI);
                    }
                    else
                    {
                        Context.Response.Cookies.Add(cookieVI);
                    }
                }
                catch (Exception)
                {
                    _logger.Warning($"Cannot set cookies: {cookieVI.Name}");
                }
            }
        }

    }
}

Comments