Take the community feedback survey now.

Quan Tran
Jan 14, 2019
  154
(0 votes)

EPiServer.Forms storing upload file in HttpSession

Few months ago I got a Forms support case from a customer. They had a concern for security perspective as well as GDPR perspective. The question is "Is it possible to avoid saving files that are uploaded using the fileupload element and attach them to the e-mail instead of uploading to the the medie library and linking to them in the e-mail ?The customer did not want to upload the files to the media library because malicious files could find their way in, and also because the file that is uploaded is very important so it needs to be attached to the e-mail as a file attachment.

Short answer is: Yes it's totally possible.

In order to avoid uploading files to media library and send attachment files instead of sending file links we have to override a couple of classes and methods.

  • The idea is that, we will save the uploaded files in HttpSession and later on when sending email in actor, we will get it back and attach them to the e-mail then remove it from HttpSession. But by doing so, we'll encounter some downsides regarding to Performance issue . Because by default, SendEmailAfterSubmissionActor runs asynchronously after form submission therefore the HttpContext.Current will be lost and we can not retrieve file from HttpSession. Thus, SendEmailAfterSubmissionActor must be run synchronously (will be overridden) to get HttpContext.Current . Storing files in HttpSession will cause performance penalties if thousands of users uploading large files at the same time .Furthermore, if file size is too large so it will not be able to attach to Email.
  • The approach above it works but not very well. Fortunately, we can implement in another way. We can upload file to third party (like GoogleDriver for example) and get it back then attach to Email.

For the sake of simplicity, I'm going to implement the first approach. The steps are as below:

1. Create a class to override InsertPostedFilesToSubmittedData method in DataSubmissionService. The first argument elementFiles contains uploaded file in HttpPostedFileBase. From there, we'll save file to HttpSession with a key. That key will be saved into HttpContext and used later on when sending mail.

/// <summary>
/// Model for storing upload file in session
/// </summary>
public class SessionStoreFile
{
    public string FileName { get; set; }
    public string Extension { get; set; }
    public byte[] Data { get; set; }
}
/// <summary>
/// Helper class for handling file
/// </summary>
public class FileHelper
{
    /// <summary>
    /// Get binary data from <see cref="HttpPostedFileBase"/>
    /// </summary>
    /// <param name="file"></param>
    /// <returns></returns>
    public static byte[] GetBlobFromPostedFile(HttpPostedFileBase file)
    {
        byte[] blob;
        using (BinaryReader reader = new BinaryReader(file.InputStream))
        {
            blob = reader.ReadBytes((Int32)file.InputStream.Length);
        }

        return blob;
    }
}
public class CustomFormsDataSubmissionService : DataSubmissionService
{
    protected override void InsertPostedFilesToSubmittedData(IEnumerable<Tuple<string, ContentReference, HttpPostedFileBase>> elementFiles, HttpContextBase httpContext, ref Dictionary<string, object> submittedData)
    {

        // IDEA : Store file in HttpSession and get it back when sending mail.AFter sending mail, remove it from session
        // If you don't want to store in Session, you can upload it to third party like GoogleDrive

        // if there's no upload file, do nothing
        if (elementFiles == null || elementFiles.Count() == 0)
        {
            return;
        }

        // using DateTime.Now.Ticks as ID of each Post
        var postId = DateTime.Now.Ticks;
        var sessionStoreFiles = new List<SessionStoreFile>();

        foreach (var item in elementFiles)
        {
            var postedFileBase = item.Item3;
            if (postedFileBase == null)
                continue;

            sessionStoreFiles.Add(new SessionStoreFile
            {
                FileName = postedFileBase.FileName,
                Extension = Path.GetExtension(postedFileBase.FileName),
                Data = FileHelper.GetBlobFromPostedFile(postedFileBase)
            });
        }

        if (sessionStoreFiles.Count == 0)
        {
            return;
        }

        var fileStoreKey = string.Format("_EpiFormUploadFile_{0}", postId);

        // save file data to Session
        var session = System.Web.HttpContext.Current.Session;
        session[fileStoreKey] = sessionStoreFiles;

        // save the file store key into HttpContext. This will be used in email actor later on.
        httpContext.Items.Add("__EpiFormUploadFile_Session_StoreKey", fileStoreKey);
    }
}

2. Create a class name SendEmailAfterSubmissionActor to override Run method of EPiServer.Forms.Implementation.Actors.SendEmailAfterSubmissionActor. Note that for some reasons, the new overridden class name must be exact SendEmailAfterSubmissionActor. Here, we will get uploaded files from HttpSession and attach it in the email then remove them from HttpSession.

using EPiServer.Forms.Implementation.Actors;
using System;
using System.Linq;
using System.Web;
using EPiServer.Logging;
using EPiServer.Forms.Core.Internal;
using EPiServer.ServiceLocation;
using EPiServer.Forms.Helpers.Internal;
using System.Text.RegularExpressions;
using System.Net.Mail;
using System.Collections.Generic;
using System.IO;
using FormsFileSession.Customization;

/// <summary>
/// Custom actor for sending email.
/// </summary>
public class SendEmailAfterSubmissionActor : EPiServer.Forms.Implementation.Actors.SendEmailAfterSubmissionActor
{
    private readonly Injected<PlaceHolderService> _placeHolderService;
    private bool _sendMessageInHTMLFormat = false;
    private static SmtpClient _smtpClient = new SmtpClient();

    /// <summary>
    /// By default, this Actor run asynchronously after forms submission
    /// In order to get file data from Session, we have to set this to FALSE so it will run synchronously right after form submission
    /// NOTE: This will increase submission time 
    /// </summary>
    public override bool IsSyncedWithSubmissionProcess { get { return true; } }

    public SendEmailAfterSubmissionActor()
    {
        _sendMessageInHTMLFormat = _formConfig.Service.SendMessageInHTMLFormat;
    }

    public override object Run(object input)
    {
        var emailConfigurationCollection = Model as IEnumerable<EmailTemplateActorModel>;
        if (emailConfigurationCollection == null || emailConfigurationCollection.Count() < 1)
        {
            _logger.Debug("There is no emailConfigurationCollection for this actor to work");
            return null;
        }

        _logger.Information("Start sending email with {0} configuration entries in emailConfigurationCollection", emailConfigurationCollection.Count());
        foreach (var emailConfiguration in emailConfigurationCollection)
        {
            SendMessage(emailConfiguration);
        }

        return null;
    }

    private void SendMessage(EmailTemplateActorModel emailConfig)
    {
        var toEmails = emailConfig.ToEmails;
        if (string.IsNullOrEmpty(toEmails))
        {
            _logger.Debug("There is no ToEmails to send. Skip.");
            return;
        }

        // replace placeholders with their value
        var bodyPlaceHolders = GetBodyPlaceHolders(null);
        var emailAddressToSend = _placeHolderService.Service.Replace(toEmails, bodyPlaceHolders, false);

        // split To, get multiple email addresses and send email to each mail address later
        var toEmailCollection = emailAddressToSend.SplitBySeparators(new[] { ",", ";", Environment.NewLine });
        if (toEmailCollection == null || toEmailCollection.Count() == 0)
        {
            _logger.Debug("There is no ToEmails to send. Skip.");
            return;
        }

        try
        {
            var subjectPlaceHolders = GetSubjectPlaceHolders(null);
            var subject = _placeHolderService.Service.Replace(emailConfig.Subject, subjectPlaceHolders, false);

            //replace line breaks by spaces in subject
            var regexLineBreak = new Regex("(\r\n|\r|\n)");
            subject = regexLineBreak.Replace(subject, " ");

            // because the subject cannot display as HTML, we need to decode it
            subject = HttpUtility.HtmlDecode(subject);

            var bodyHtmlString = emailConfig.Body == null ? string.Empty : _formBusinessService.Service.ToHtmlStringWithFriendlyUrls(emailConfig.Body);

            // body is inputted via tinyEMC will be saved as a HTML-encoded string, we should decode it before replacing with placeholders
            var decodedBody = HttpUtility.HtmlDecode(bodyHtmlString);
            var body = _placeHolderService.Service.Replace(decodedBody, bodyPlaceHolders, false);

            var message = new MailMessage();
            message.Subject = subject;
            message.Body = RewriteUrls(body);
            message.IsBodyHtml = _sendMessageInHTMLFormat;

            #region ATTACHMENT_FILES
            ////////////////////////// ATTACHMENT ///////////////////////////

            // get session store key of upload file
            var httpContext = new System.Web.HttpContextWrapper(System.Web.HttpContext.Current) as HttpContextBase;

            if (httpContext.Items.Contains("__EpiFormUploadFile_Session_StoreKey"))
            {
                var fileStoreKey = (string)httpContext.Items["__EpiFormUploadFile_Session_StoreKey"];
                var httpSession = System.Web.HttpContext.Current.Session; // this.HttpRequestContext.RequestContext.HttpContext.Session;
                if (httpSession != null)
                {
                    var files = (List<SessionStoreFile>)httpSession[fileStoreKey];
                    if (files != null && files.Count > 0)
                    {
                        foreach (var file in files)
                        {
                            message.Attachments.Add(new Attachment(new MemoryStream(file.Data), file.FileName));
                        }
                    }

                    // remove file in session
                    httpSession[fileStoreKey] = null;
                    httpSession.Remove(fileStoreKey);
                }
            }

            /////////////////////////// END ATTACHMENT /////////////////////////
            #endregion

            if (!string.IsNullOrEmpty(emailConfig.FromEmail))
            {
                var emailAddressFrom = _placeHolderService.Service.Replace(emailConfig.FromEmail, subjectPlaceHolders, false);
                message.From = new MailAddress(emailAddressFrom);
            }

            foreach (var emailAddress in toEmailCollection)
            {
                try
                {
                    // email from the EndUser cannot be trust, wrong email format can not be added.
                    message.To.Add(new MailAddress(emailAddress));
                }
                catch (Exception ex)
                {
                    _logger.Debug(string.Format(@"{0} is not valid email addresses for email.To", emailAddress), ex);
                }
            }

            _smtpClient.Send(message);
        }
        catch (Exception ex)
        {
            _logger.Error("Failed to send email", ex);
        }
    }
}

3. We have to disable the built-in SendEmailAfterSubmissionActor actor to prevent sending email twice.

using EPiServer.Forms.Core.PostSubmissionActor;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using EPiServer.Forms.Core.Models;
using EPiServer.Forms.Implementation.Elements;

/// <summary>
/// Provide submission data for Actors, instantiate and execute actors
/// </summary>
public class CustomActorsExecutingService : ActorsExecutingService
{
    public override IEnumerable<IPostSubmissionActor> GetFormSubmissionActors(Submission submission, FormContainerBlock formContainer, FormIdentity formIden, HttpRequestBase request, HttpResponseBase response, bool isFormFinalizedSubmission)
    {
        var submissionActors = base.GetFormSubmissionActors(submission, formContainer, formIden, request, response, isFormFinalizedSubmission);

        if (submissionActors == null || submissionActors.Count() == 0)
        {
            return Enumerable.Empty<IPostSubmissionActor>();
        }

        var filteredActors = submissionActors.ToList();
        // remove the system's email-actor
        filteredActors.RemoveAll(act => act.Name == typeof(EPiServer.Forms.Implementation.Actors.SendEmailAfterSubmissionActor).FullName);

        return filteredActors;
    }
}

4. And don't forget to register dependencies

public void ConfigureContainer(ServiceConfigurationContext context)
{
    //Implementations for custom interfaces can be registered here.

    context.ConfigurationComplete += (o, e) =>
    {
        context.Services.
             AddTransient<EPiServer.Forms.Core.Internal.DataSubmissionService, CustomFormsDataSubmissionService>()
            .AddTransient<EPiServer.Forms.Implementation.Actors.SendEmailAfterSubmissionActor, SendEmailAfterSubmissionActor>()
            .AddTransient<ActorsExecutingService, CustomActorsExecutingService>();
    };
}

If you have any questions or better approach, please drop me a comment :)

Jan 14, 2019

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP - Opticon London 2025

This installment of a day in the life of an Optimizely OMVP gives an in-depth coverage of my trip down to London to attend Opticon London 2025 held...

Graham Carr | Oct 2, 2025

Optimizely Web Experimentation Using Real-Time Segments: A Step-by-Step Guide

  Introduction Personalization has become de facto standard for any digital channel to improve the user's engagement KPI’s.  Personalization uses...

Ratish | Oct 1, 2025 |

Trigger DXP Warmup Locally to Catch Bugs & Performance Issues Early

Here’s our documentation on warmup in DXP : 🔗 https://docs.developers.optimizely.com/digital-experience-platform/docs/warming-up-sites What I didn...

dada | Sep 29, 2025

Creating Opal Tools for Stott Robots Handler

This summer, the Netcel Development team and I took part in Optimizely’s Opal Hackathon. The challenge from Optimizely was to extend Opal’s abiliti...

Mark Stott | Sep 28, 2025

Integrating Commerce Search v3 (Vertex AI) with Optimizely Configured Commerce

Introduction This blog provides a technical guide for integrating Commerce Search v3, which leverages Google Cloud's Vertex AI Search, into an...

Vaibhav | Sep 27, 2025

A day in the life of an Optimizely MVP - Opti Graph Extensions add-on v1.0.0 released

I am pleased to announce that the official v1.0.0 of the Opti Graph Extensions add-on has now been released and is generally available. Refer to my...

Graham Carr | Sep 25, 2025