Changing the Visitor Submit Timeout in Optimizely Forms for CMS 12

When working with Optimizely Forms, you'll find there are multiple ways to configure the functionality of a form. One of those options an editor can configure is "Allow multiple submissions from the same IP/cookie", which is commonly used to prevent/deny a visitor from resubmitting the form, and it works by creating a browser cookie with the form's information.

While this can be a useful option to have to prevent a visitor from continually resubmitting a form, it's not foolproof. A visitor could easily resubmit the form in incognito mode (private browsing), or simply clear their browser cookies to resubmit the form, or just wait until the cookie expires.

But what if you want to allow the user to resubmit the form after a certain amount of time? Let's look at how we can change the timeout for a form resubmission.

Default Configuration

By default, the timeout for a form resubmission is set to 90 days. This is a good default value, but it may not be the best value for your specific use case. You may want to change this value to something shorter, such as 30 days, 7 days, or even 1 day.

The easiest way to change this value is to update the appsettings.json file, providing the new value in the FormsConfig section. Here's an example of how you can set the value to 1 day:

{
	"EPiServer": {
		"Forms": {
			"FormsConfig": {
				"VisitorSubmitTimeout": 1
			}
		}
	}
}

An alternative way to set the value is to set the value in the ConfigureServices function in Startup.cs. Here's an example of how you can set the value to 1 day:

services.Configure<FormsConfigOptions>(options => options.VisitorSubmitTimeout = 1);

There are many more settings available within the FormsConfigOptions class. You can find the full list of options in the Optimizely Forms documentation.

But what if you want to change this value to be shorter than 1 day? You can't do that with the FormsConfig section, because the default functionality only considers that value to be in terms of days, and the VisitorSubmitTimeout property must be an integer type. If you want to set the value to be shorter than 1 day, you'll need to create a custom implementation.

Switching from Days to Seconds

To change the value from days to seconds, you'll need to extend the functionality found in the ProgressiveSubmitInfoService class, which is in the EPiServer.Forms.Core.Internal namespace.

I do need to mention that this class is in an internal namespace, which means it's not part of the supported public API.

Such classes can still be used, but they are not covered by the semantic versioning promise and can therefore change between minor releases without prior warning. Any problems that arise from using such classes are also not covered by our usual support.

In other words, this means that the class could change in future versions of Optimizely Forms, and your implementation could break. However, if you're willing to take that risk, here's how you can do it.

First, we'll need to extend the ProgressiveSubmitInfoService class and override the SetProgressiveSubmitInfo function. In this extended class, we'll just change the cookie's Expires option to add seconds to the current DateTime instead of days.

public class ExtendedProgressiveSubmitInfoService : ProgressiveSubmitInfoService
{
	public override void SetProgressiveSubmitInfo(Guid formContentGuid, HttpContext httpContext, ProgressiveSubmitInfo progressiveSubmitInfo, string formLanguage)
	{
		var visitorIdentifier = _visitorIdentifyService.Service.GetVisitorIdentifyProvider(httpContext).GetVisitorIdentifier();

		if (string.IsNullOrWhiteSpace(visitorIdentifier))
		{
			return;
		}

		string progressiveSubmitCookieKey = GetProgressiveSubmitCookieKey(formContentGuid, visitorIdentifier, formLanguage);

		httpContext.Response.Cookies.Append(progressiveSubmitCookieKey, progressiveSubmitInfo.ToJson(), new CookieOptions()
		{
			HttpOnly = true,
			Path = "/",

			// Default functionality uses days
			//Expires = new DateTimeOffset?((DateTimeOffset)DateTime.Now.AddDays(_formConfig.Service.VisitorSubmitTimeout)),

			// New functionality uses seconds
			Expires = new DateTimeOffset?((DateTimeOffset)DateTime.Now.AddSeconds(_formConfig.Service.VisitorSubmitTimeout)),

			Secure = httpContext.Request.IsHttps
		});
	}
}

Using this Extended Class

To use this extended class, we unfortunately can't simply register it in the ConfigureServices function in Startup.cs. We'll need to create an IConfigurableModule, ensuring it has a dependency on the EPiServer.Forms.InitializationModule initialization. This will allow us to override the default implementation and use our ExtendedProgressiveSubmitInfoService class.

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Forms.InitializationModule))]
public class FormsInitialization : IConfigurableModule
{
	public void ConfigureContainer(ServiceConfigurationContext context)
	{
		context.ConfigurationComplete += (o, e) =>
		{
			context.Services.AddSingleton<ProgressiveSubmitInfoService, ExtendedProgressiveSubmitInfoService>();
		};
	}

	public void Initialize(InitializationEngine context) { }
	public void Uninitialize(InitializationEngine context) { }
}

Making This More Editor Configurable

The one issue with this approach is that all forms use this same VisitorSubmitTimeout value. There may be times when you want to set a different value for different forms. If you want to make this more editor-configurable, there's a bit more work that needs to happen.

First, you'll need to have custom FormContainerBlock. There is documentation available that shows how this can be done.

To that custom FormContainerBlock, you'll need to add a new property that allows the editor to set a VisitorSubmitTimeout value:

public class ExtendedFormContainerBlock : FormContainerBlock
{
	[Display(Name = "Visitor Submit Timeout (In seconds)", GroupName = SiteConstants.GroupNames.Settings, Order = 1,
		Description = "Used when 'Allow multiple submissions from the same IP/cookie' is not checked. Default value is 90 days.")]
	[Range(0, int.MaxValue, ErrorMessage = "Only positive number allowed.")]
	public virtual double VisitorSubmitTimeout { get; set; }
}

Finally, we'll change the ExtendedProgressiveSubmitInfoService class to use this new value. We'll also change back the default functionality to use days as well:

public class ExtendedProgressiveSubmitInfoService : ProgressiveSubmitInfoService
{
	protected Injected<IContentLoader> _contentLoader;

	public override void SetProgressiveSubmitInfo(Guid formContentGuid, HttpContext httpContext, ProgressiveSubmitInfo progressiveSubmitInfo, string formLanguage)
	{
		var visitorIdentifier = _visitorIdentifyService.Service.GetVisitorIdentifyProvider(httpContext).GetVisitorIdentifier();

		if (string.IsNullOrWhiteSpace(visitorIdentifier))
		{
			return;
		}

		string progressiveSubmitCookieKey = GetProgressiveSubmitCookieKey(formContentGuid, visitorIdentifier, formLanguage);

		httpContext.Response.Cookies.Append(progressiveSubmitCookieKey, progressiveSubmitInfo.ToJson(), new CookieOptions()
		{
			HttpOnly = true,
			Path = "/",
			Expires = GetExpires(formContentGuid), // Compared to default functionality, this line is the only change
			Secure = httpContext.Request.IsHttps
		});
	}

	private DateTimeOffset? GetExpires(Guid formContentGuid)
	{
		// Default functionality sets expires to X days from now, where X is the value of the form configuration's VisitorSubmitTimeout property
		var expires = new DateTimeOffset?((DateTimeOffset)DateTime.Now.AddDays(_formConfig.Service.VisitorSubmitTimeout));

		// Extended functionality sets expires to X seconds from now, where X is the value of the form instance's VisitorSubmitTimeout property
		var currentForm = _contentLoader.Service.Get<IContent>(formContentGuid) as ExtendedFormContainerBlock.ExtendedFormContainerBlock;

		if (currentForm != null && currentForm.VisitorSubmitTimeout > 0)
		{
			expires = new DateTimeOffset?((DateTimeOffset)DateTime.Now.AddSeconds(currentForm.VisitorSubmitTimeout));
		}

		return expires;
	}
}

With this, you can now set a different VisitorSubmitTimeout value for each form, allowing visitors to resubmit the form but after a much shorter time.