Force Download and Prompt User to Save Media from the Sitecore Media Library

Recently on Slack Chat a question got asked by fellow MVP Jason Bert about how to prompt a user to save an item from the Sitecore Media Library. Whether a user is prompted to download media depends on a few factors:

  • Whether the browser or an associated plugin understands how to process that mime type
  • Settings under mediaLibrary\mediaTypes in the Sitecore section of config
  • The response headers sent by the server, and whether Content-Disposition=attachment

It’s this final header which we need to add ourselves in the Response headers. I shared some code but was pretty surprised when it did not work. Previously in an earlier version of Sitecore I had simply tapped into the getMediaStream pipeline and then set Content-Disposition=true header based on a url parameter. Something had obviously changed, or I was going mad. Most likely a combination of both…

Brute force technique

There’s nothing wrong with brute force, but it relies on the fact that specific mime types will ALWAYS be downloaded. If that’s the case then it’s a simple matter of updating the config.

For example, if you take a look at the PDF file type settings:

<mediaType name="PDF file" extensions="pdf">
  <mimeType>application/pdf</mimeType>
  <forceDownload>true</forceDownload>
  <sharedTemplate>system/media/unversioned/pdf</sharedTemplate>
  <versionedTemplate>system/media/versioned/pdf</versionedTemplate>
</mediaType>

The forceDownload=true is what causes the prompt for users to always save the file (I previously wrote a blog post about why this was changed and how to change it back). You can do this for any mime type you wish and Sitecore will amend the Content-Disposition accordingly.

Forcing Download on Client Side

It’s possible to force the save prompt using the HTML5 download attribute on your anchor links, which most modern browsers support. Using this you don’t even need any code, it’s all magically done by the browser.

Unfortunately, trusty old IE keeps billing those hours for us…

Conditional Downloads

It’s fairly obvious that you don’t want to always force download of images for example. It may the case that you want to link to a very high resolution image, possibly some marketing material, that you want to prompt the download of.

Let’s get straight into it. Let’s create a new event handler, no inhertance needed.

using System;
using System.Linq;
using System.Net;
using Sitecore.Data.Items;
using Sitecore.Events;
using Sitecore.Resources.Media;
using Sitecore.StringExtensions;

namespace Sitecore.Custom.Pipelines
{
    public class DownloadProcessor
    {
        public void OnMediaRequest(object sender, EventArgs args)
        {
            // Check if the request was for a download, else break out early
            if (!Sitecore.MainUtil.GetBool(Sitecore.Web.WebUtil.GetQueryString("download"), false))
                return;

            // Safety checks
            if (Sitecore.Context.Site.Name.Equals(Sitecore.Constants.ShellSiteName, StringComparison.InvariantCultureIgnoreCase))
                return;
            var sitecoreEventArgs = (SitecoreEventArgs)args;
            if (sitecoreEventArgs == null || !sitecoreEventArgs.Parameters.Any())
                return;
            var request = (MediaRequest)sitecoreEventArgs.Parameters[0];
            if (request == null)
                return;

            // Now we've established we have a valid request
            ForceMediaDownload(request);
        }

        /// <summary>
        /// Forces download of the requested media item prompting the user to save the file
        /// </summary>
        /// <param name="request">The MediaRequest</param>
        private void ForceMediaDownload(MediaRequest request)
        {
            var mediaItem = MediaManager.GetMedia(request.MediaUri).MediaData.MediaItem;

            var response = request.InnerRequest.RequestContext.HttpContext.Response;
            response.Clear();
            response.ContentType = mediaItem.MimeType;
            response.Headers.Set("Content-Disposition", "attachment; filename=" + GetFileName(mediaItem));
            response.StatusCode = (int)HttpStatusCode.OK;
            response.BufferOutput = true;
            mediaItem.GetMediaStream().CopyTo(response.OutputStream);
            response.Flush();
            response.End();
        }

        private static string GetFileName(MediaItem mi)
        {
            // Some versions of IE don't like the spaces in the names Surprise!
            return ("{0}.{1}".FormatWith(mi.Name, mi.Extension)).Replace(" ", "-");
        }
    }
}

All we have to do is patch in our new handler into the media:request event:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <events>
      <event name="media:request">
        <handler type="Sitecore.Custom.Pipelines.DownloadProcessor, Sitecore.Custom" method="OnMediaRequest"/>
      </event>
    </events>
  </sitecore>
</configuration>

What’s this event? It’s not in default Sitecore, but it will fire since the event is raised from MediaRequestHandler. Just add it in, it’s a hidden feature 🙂 After a quick search it seems MVP Anders Laub also previously used this in a blog post and I was able to borrow the above safety checks from his code. Double bonus!

Rendering Links

Now all you need to do is ensure that the URL parameter download=1 is appended to any links you wish to force save prompt for, e.g. /-/media/Koala.jpg?download=1.

Experience Editor / Rich Text Editor Support

Creating a link to a media item is super simple. Just click the Add Internal Link button, select the 2nd tab [Media Library] and pick your item. The resulting rendered HTML will look like this:

<a href="/-/media/Koala.jpg">Koala</a>

In order to force the download, you need append the url parameters. This means that the user has to switch to html mode and manually add it in. Even with the plain HTML5 browser-only technique you need to manually add the download attribute. That’s fine for your power users, not so user friendly for the rest though.

As a workaround, we can apply an additional CSS class to the links and then use a simple bit of Javascript to detect support for the HTML5 download attribute otherwise append the URL parameter:

var downloadAttrSupported = ("download" in document.createElement("a"));
$('.download').each(function(){
  if (downloadAttrSupported) {
    $(this).attr('download','');
  } else {
    this.href = this.href+'?download=1';
  }
});

jsFiddle demo

It’s not the best piece of Javascript / jQuery, you would want to check if there are any existing parameters and append with an ampersand, but it just serves to illustrate my point

Your editors can now simply add a CSS class to the links easily in the Rich Text fields or General Link field and magic will happen. All well behaved browsers will use the default Sitecore functionality and IE will follow through and use the new handler we defined.

Advertisements

Leave a Reply

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s