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'; } });
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.