Yesterday evening — a Friday evening by the way (what, you don’t code on Friday evenings? 😉 ) — I wanted to have a bit of fun by building some sort of customization in Sitecore but was struggling on what to build.

After about an hour of pondering, it dawned on me: I was determined to build a custom Content Editor Image field that gives content authors the ability to download images from a supplied URL; save the image to disk; upload the image into the Media Libary; and then set it on a custom Image field of an Item.

I’m sure someone has built something like this in the past and may have even uploaded a module that does this to the Sitecore Marketplace — I didn’t really look into whether this had already been done before since I wanted to have some fun by taking on the challenge. What follows is the fruit of that endeavor.

Before I move forward, I would like to caution you on using the code that follows — I have not rigorously tested this code at all so use at your own risk.

Before I began coding, I thought about how I wanted to approach this challenge. I decided I would build a custom Sitecore pipeline to handle this code. Why? Well, quite frankly, it gives you flexibility on customization, and also native Sitecore code is hugely composed of pipelines — why deviate from the framework?

First, I needed a class whose instances would serve as the custom pipeline’s arguments object. The following class was built for that:

using Sitecore.Data; using Sitecore.Pipelines; namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary { public class DownloadImageToMediaLibraryArgs : PipelineArgs { public Database Database { get; set; } public string ImageFileName { get; set; } public string ImageFilePath { get; set; } public string ImageItemName { get; set; } public string ImageUrl { get; set; } public string MediaId { get; set; } public string MediaLibaryFolderPath { get; set; } public string MediaPath { get; set; } public bool FileBased { get; set; } public bool IncludeExtensionInItemName { get; set; } public bool OverwriteExisting { get; set; } public bool Versioned { get; set; } } }

I didn’t just start off with all of the properties you see on this class — it was an iterative process where I had to go back, add more and even remove some that were no longer needed. You will see why I have these on it from the code below.

I decided to employ the Template method pattern in this code, and defined the following abstract base class which all processors of my custom pipeline will sub-class:

using Sitecore.Diagnostics; namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary { public abstract class DownloadImageToMediaLibraryProcessor { public void Process(DownloadImageToMediaLibraryArgs args) { Assert.ArgumentNotNull(args, "args"); if(!CanProcess(args)) { AbortPipeline(args); return; } Execute(args); } protected abstract bool CanProcess(DownloadImageToMediaLibraryArgs args); protected virtual void AbortPipeline(DownloadImageToMediaLibraryArgs args) { args.AbortPipeline(); } protected abstract void Execute(DownloadImageToMediaLibraryArgs args); } }

All processors of the custom pipeline will have to implement the CanProcess and Execute methods above, and also have the ability to redefine the AbortPipeline method if needed.

The main magic for all processors happen in the Process method above — if the processor can process the data supplied via the arguments object, then it will do so using the Execute method. Otherwise, the pipeline will be aborted via the AbortPipeline method.

The following class serves as the first processor of the custom pipeline.

using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.IO; namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary { public class SetProperties : DownloadImageToMediaLibraryProcessor { private string UploadDirectory { get; set; } protected override bool CanProcess(DownloadImageToMediaLibraryArgs args) { Assert.IsNotNullOrEmpty(UploadDirectory, "UploadDirectory must be set in configuration!"); Assert.IsNotNull(args.Database, "args.Database must be supplied!"); return !string.IsNullOrWhiteSpace(args.ImageUrl) && !string.IsNullOrWhiteSpace(args.MediaLibaryFolderPath); } protected override void Execute(DownloadImageToMediaLibraryArgs args) { args.ImageFileName = GetFileName(args.ImageUrl); args.ImageItemName = GetImageItemName(args.ImageUrl); args.ImageFilePath = GetFilePath(args.ImageFileName); } protected virtual string GetFileName(string url) { Assert.ArgumentNotNullOrEmpty(url, "url"); return FileUtil.GetFileName(url); } protected virtual string GetImageItemName(string url) { Assert.ArgumentNotNullOrEmpty(url, "url"); string fileNameNoExtension = GetFileNameNoExtension(url); if(string.IsNullOrWhiteSpace(fileNameNoExtension)) { return string.Empty; } return ItemUtil.ProposeValidItemName(fileNameNoExtension); } protected virtual string GetFileNameNoExtension(string url) { Assert.ArgumentNotNullOrEmpty(url, "url"); return FileUtil.GetFileNameWithoutExtension(url); } protected virtual string GetFilePath(string fileName) { Assert.ArgumentNotNullOrEmpty(fileName, "fileName"); return string.Format("{0}/{1}", FileUtil.MapPath(UploadDirectory), fileName); } } }

Instances of the above class will only run if an upload directory is supplied via configuration (see the patch include configuration file down below); a Sitecore Database is supplied (we have to upload this image somewhere); an image URL is supplied (can’t download an image without this); and a Media Library folder is supplied (where are we storing this image?).

The Execute method then sets additional properties on the arguments object that the next processors will need in order to complete their tasks.

The following class serves as the second processor of the custom pipeline. This processor will download the image from the supplied URL:

using System.Net; namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary { public class DownloadImage : DownloadImageToMediaLibraryProcessor { protected override bool CanProcess(DownloadImageToMediaLibraryArgs args) { return !string.IsNullOrWhiteSpace(args.ImageUrl) && !string.IsNullOrWhiteSpace(args.ImageFilePath); } protected override void Execute(DownloadImageToMediaLibraryArgs args) { using (WebClient client = new WebClient()) { client.DownloadFile(args.ImageUrl, args.ImageFilePath); } } } }

The processor instance of the above class will only execute when an image URL is supplied and a location on the file system is given — this is the location on the file system where the image will live before being uploaded into the Media Library.

If all checks out, the image is downloaded from the given URL into the specified location on the file system.

The next class serves as the third processor of the custom pipeline. This processor will upload the image on disk to the Media Library:

using System.IO; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Resources.Media; using Sitecore.Sites; namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary { public class UploadImageToMediaLibrary : DownloadImageToMediaLibraryProcessor { private string Site { get; set; } protected override bool CanProcess(DownloadImageToMediaLibraryArgs args) { Assert.IsNotNullOrEmpty(Site, "Site must be set in configuration!"); return !string.IsNullOrWhiteSpace(args.MediaLibaryFolderPath) && !string.IsNullOrWhiteSpace(args.ImageItemName) && !string.IsNullOrWhiteSpace(args.ImageFilePath) && args.Database != null; } protected override void Execute(DownloadImageToMediaLibraryArgs args) { MediaCreatorOptions options = new MediaCreatorOptions { Destination = GetMediaLibraryDestinationPath(args), FileBased = args.FileBased, IncludeExtensionInItemName = args.IncludeExtensionInItemName, OverwriteExisting = args.OverwriteExisting, Versioned = args.Versioned, Database = args.Database }; MediaCreator creator = new MediaCreator(); MediaItem mediaItem; using (SiteContextSwitcher switcher = new SiteContextSwitcher(GetSiteContext())) { using (FileStream fileStream = File.OpenRead(args.ImageFilePath)) { mediaItem = creator.CreateFromStream(fileStream, args.ImageFilePath, options); } } if (mediaItem == null) { AbortPipeline(args); return; } args.MediaId = mediaItem.ID.ToString(); args.MediaPath = mediaItem.MediaPath; } protected virtual SiteContext GetSiteContext() { SiteContext siteContext = SiteContextFactory.GetSiteContext(Site); Assert.IsNotNull(siteContext, string.Format("The site: {0} does not exist!", Site)); return siteContext; } protected virtual string GetMediaLibraryDestinationPath(DownloadImageToMediaLibraryArgs args) { return string.Format("{0}/{1}", args.MediaLibaryFolderPath, args.ImageItemName); } } }

The processor instance of the class above will only run when we have a Media Library folder location; an Item name for the image; a file system path for the image; and a Database to upload the image to. I also ensure a “site” is supplied via configuration so that I can switch the site context — when using the default of “shell”, I was being brought to the image Item in the Media Library after it was uploaded which was causing the image not to be set on the custom Image field on the Item.

If everything checks out, we upload the image to the Media Library in the specified location.

The next class serves as the last processor of the custom pipeline. This processor just deletes the image from the file system (why keep it around since we are done with it?):

using Sitecore.IO; namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary { public class DeleteImageFromFileSystem : DownloadImageToMediaLibraryProcessor { protected override bool CanProcess(DownloadImageToMediaLibraryArgs args) { return !string.IsNullOrWhiteSpace(args.ImageFilePath) && FileUtil.FileExists(args.ImageFilePath); } protected override void Execute(DownloadImageToMediaLibraryArgs args) { FileUtil.Delete(args.ImageFilePath); } } }

The processor instance of the class above can only delete the image if its path is supplied and the file exists.

If all checks out, the image is deleted.

The next class is the class that serves as the custom Image field:

using System; using System.Net; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Pipelines; using Sitecore.Shell.Framework; using Sitecore.Web.UI.Sheer; using Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor { public class Image : Sitecore.Shell.Applications.ContentEditor.Image { public Image() : base() { } public override void HandleMessage(Message message) { Assert.ArgumentNotNull(message, "message"); if (string.Equals(message.Name, "contentimage:download", StringComparison.CurrentCultureIgnoreCase)) { GetInputFromUser(); return; } base.HandleMessage(message); } protected void GetInputFromUser() { RunProcessor("GetImageUrl", new ClientPipelineArgs()); } protected virtual void GetImageUrl(ClientPipelineArgs args) { if (!args.IsPostBack) { SheerResponse.Input("Enter the url of the image to download:", string.Empty); args.WaitForPostBack(); } else if (args.HasResult && IsValidUrl(args.Result)) { args.Parameters["imageUrl"] = args.Result; args.IsPostBack = false; RunProcessor("ChooseMediaLibraryFolder", args); } else { CancelOperation(args); } } protected virtual bool IsValidUrl(string url) { if (string.IsNullOrWhiteSpace(url)) { return false; } try { HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url); request.Method = "HEAD"; request.GetResponse(); } catch (Exception ex) { SheerResponse.Alert("The specified url is not valid. Please try again."); return false; } return true; } protected virtual void RunProcessor(string processor, ClientPipelineArgs args) { Assert.ArgumentNotNullOrEmpty(processor, "processor"); Sitecore.Context.ClientPage.Start(this, processor, args); } public void ChooseMediaLibraryFolder(ClientPipelineArgs args) { if (!args.IsPostBack) { Dialogs.BrowseItem ( "Select A Media Library Folder", "Please select a media library folder to store this image.", "Applications/32x32/folder_into.png", "OK", "/sitecore/media library", string.Empty ); args.WaitForPostBack(); } else if (args.HasResult) { Item folder = Client.ContentDatabase.Items[args.Result]; args.Parameters["mediaLibaryFolderPath"] = folder.Paths.FullPath; RunProcessor("DownloadImage", args); } else { CancelOperation(args); } } protected virtual void DownloadImage(ClientPipelineArgs args) { DownloadImageToMediaLibraryArgs downloadArgs = new DownloadImageToMediaLibraryArgs { Database = Client.ContentDatabase, ImageUrl = args.Parameters["imageUrl"], MediaLibaryFolderPath = args.Parameters["mediaLibaryFolderPath"] }; CorePipeline.Run("downloadImageToMediaLibrary", downloadArgs); SetMediaItemInField(downloadArgs); } protected virtual void SetMediaItemInField(DownloadImageToMediaLibraryArgs args) { Assert.ArgumentNotNull(args, "args"); if(string.IsNullOrWhiteSpace(args.MediaId) || string.IsNullOrWhiteSpace(args.MediaPath)) { return; } XmlValue.SetAttribute("mediaid", args.MediaId); Value = args.MediaPath; Update(); SetModified(); } protected virtual void CancelOperation(ClientPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); args.AbortPipeline(); } } }

The class above subclasses the Sitecore.Shell.Applications.ContentEditor.Image class — this lives in Sitecore.Kernel.dll — which is the “out of the box” Content Editor Image field. The Sitecore.Shell.Applications.ContentEditor.Image class provides hooks that we can override in order to augment functionality which I am doing above.

The magic of this class starts in the HandleMessage method — I intercept the message for a Menu item option that I define below for downloading an image from a URL.

If we are to download an image from a URL, we first prompt the user for a URL via the GetImageUrl method using a Sheer UI api call (note: I am running these methods as one-off client pipeline processors as this is the only way you can get Sheer UI to run properly).

If we have a valid URL, we then prompt the user for a Media Library location via another Sheer UI dialog (this is seen in the ChooseMediaLibraryFolder method).

If the user chooses a location in the Media Library, we then call the DownloadImage method as a client pipeline processor — I had to do this since I was seeing some weird behavior on when the image was being saved into the Media Library — which invokes the custom pipeline for downloading the image to the file system; uploading it into the Media Library; and then removing it from disk.

I then duct-taped everything together using the following patch include configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <controlSources> <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="sandbox-content"/> </controlSources> <overrideDialogs> <override dialogUrl="/sitecore/shell/Applications/Item%20browser.aspx" with="/sitecore/client/applications/dialogs/InsertSitecoreItemViaTreeDialog"> <patch:delete/> </override> </overrideDialogs> <pipelines> <downloadImageToMediaLibrary> <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.SetProperties, Sitecore.Sandbox"> <UploadDirectory>/upload</UploadDirectory> </processor> <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.DownloadImage, Sitecore.Sandbox" /> <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.UploadImageToMediaLibrary, Sitecore.Sandbox"> <Site>website</Site> </processor> <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.DeleteImageFromFileSystem, Sitecore.Sandbox" /> </downloadImageToMediaLibrary> </pipelines> </sitecore> </configuration>

One thing to note in the above file: I’ve disabled the SPEAK dialog for the Item Browser — you can see this in the <overrideDialogs> xml element — as I wasn’t able to set messaging text on it but could do so using the older Sheer UI dialog.

Now that all code is in place, we need to tell Sitecore that we have a new Image field. I do so by defining it in the Core database:

We also need a new Menu item for the “Download Image” link:

Let’s take this for a spin!

I added a new field using the custom Image field type to my Sample item template:

As you can see, we have this new Image field on my “out of the box” home item. Let’s click the “Download Image” link:

I was then prompted with a dialog to supply an image URL. I pasted one I found on the internet:

After clicking “OK”, I was prompted with another dialog to choose a Media Library location for storing the image. I chose some random folder:

After clicking “OK” on that dialog, the image was magically downloaded from the internet; uploaded into the Media Library; and set in the custom Image field on my home item:

If you have any comments, thoughts or suggestions on this, please drop a comment.

Addendum:

It’s not a good idea to use the /upload directory for temporarily storing download images — see this post for more details.

If you decide to use this solution — by the way, use this solution at your own risk 😉 — you will have to change the /upload directory in the patch include configuration file above.