Five New Optimizely Certifications are Here! Validate your expertise and advance your career with our latest certification exams. Click here to find out more

Quan Tran
Jan 14, 2019
  78
(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
Optimizely Configured Commerce and Spire CMS - Figuring out Handlers

I recently entered the world of Optimizely Configured Commerce and Spire CMS. Intriguing, interesting and challenging at the same time, especially...

Ritu Madan | Mar 12, 2025

Another console app for calling the Optimizely CMS REST API

Introducing a Spectre.Console.Cli app for exploring an Optimizely SaaS CMS instance and to source code control definitions.

Johan Kronberg | Mar 11, 2025 |

Extending UrlResolver to Generate Lowercase Links in Optimizely CMS 12

When working with Optimizely CMS 12, URL consistency is crucial for SEO and usability. By default, Optimizely does not enforce lowercase URLs, whic...

Santiago Morla | Mar 7, 2025 |

Optimizing Experiences with Optimizely: Custom Audience Criteria for Mobile Visitors

In today’s mobile-first world, delivering personalized experiences to visitors using mobile devices is crucial for maximizing engagement and...

Nenad Nicevski | Mar 5, 2025 |

Unable to view Optimizely Forms submissions when some values are too long

I discovered a form where the form submissions could not be viewed in the Optimizely UI, only downloaded. Learn how to fix the issue.

Tomas Hensrud Gulla | Mar 4, 2025 |

CMS 12 DXP Migrations - Time Zones

When it comes to migrating a project from CMS 11 and .NET Framework on the DXP to CMS 12 and .NET Core one thing you need to be aware of is the...

Scott Reed | Mar 4, 2025