Home » 2013
Yearly Archives: 2013
Restart the Sitecore Client and Server Using Custom Pipelines
Last week Michael West asked me about creating shortcuts to restart the Sitecore client and server via this tweet, and I was immediately curious myself on what was needed to accomplish this.
If you are unfamiliar with restarting the Sitecore client and server, these are options that are presented to Sitecore users — typically developers — after installing a package into Sitecore:
Until last week, I never understood how either of these checkboxes worked, and uncovered the following code during my research:
Michael West had conveyed how he wished these two lines of code lived in pipelines, and that prompted me to write this article — basically encapsulate the logic above into two custom pipelines: one to restart the Sitecore client, and the other to restart the Sitecore server.
I decided to define the concept of a ‘Restarter’, an object that restarts something — this could be anything — and defined the following interface for such objects:
namespace Sitecore.Sandbox.Utilities.Restarters { public interface IRestarter { void Restart(); } }
I then created the following IRestarter for the Sitecore client:
using Sitecore; using Sitecore.Diagnostics; using Sitecore.Web.UI.Sheer; namespace Sitecore.Sandbox.Utilities.Restarters { public class SitecoreClientRestarter : IRestarter { private ClientResponse ClientResponse { get; set; } public SitecoreClientRestarter() : this(Context.ClientPage) { } public SitecoreClientRestarter(ClientPage clientPage) { SetClientResponse(clientPage); } public SitecoreClientRestarter(ClientResponse clientResponse) { SetClientResponse(clientResponse); } private void SetClientResponse(ClientPage clientPage) { Assert.ArgumentNotNull(clientPage, "clientPage"); SetClientResponse(clientPage.ClientResponse); } private void SetClientResponse(ClientResponse clientResponse) { Assert.ArgumentNotNull(clientResponse, "clientResponse"); ClientResponse = clientResponse; } public void Restart() { ClientResponse.Broadcast(ClientResponse.SetLocation(string.Empty), "Shell"); } } }
The class above has three constructors. One constructor takes an instance of Sitecore.Web.UI.Sheer.ClientResponse — this lives in Sitecore.Kernel.dll — and another constructor takes in an instance of
Sitecore.Web.UI.Sheer.ClientPage — this also lives in Sitecore.Kernel.dll — which contains a property instance of Sitecore.Web.UI.Sheer.ClientResponse, and this instance is set on the ClientResponse property of the SitecoreClientRestarter class.
The third constructor — which is parameterless — calls the constructor that takes in a Sitecore.Web.UI.Sheer.ClientPage instance, and passes the ClientResponse instance set in Sitecore.Context.
I followed building the above class with another IRestarter — one that restarts the Sitecore server:
using Sitecore.Install; namespace Sitecore.Sandbox.Utilities.Restarters { public class SitecoreServerRestarter : IRestarter { public SitecoreServerRestarter() { } public void Restart() { Installer.RestartServer(); } } }
There really isn’t much happening in the class above. It just calls the static method RestartServer() — this method changes the timestamp on the Sitecore instance’s Web.config to trigger a web application restart — on Sitecore.Install.Installer in Sitecore.Kernel.dll.
Now we need to a way to use the IRestarter classes above. I built the following class to serve as a processor of custom pipelines I define later on in this post:
using Sitecore.Diagnostics; using Sitecore.Pipelines; using Sitecore.Sandbox.Utilities.Restarters; namespace Sitecore.Sandbox.Pipelines.RestartRestarter { public class RestartRestarterOperations { private IRestarter Restarter { get; set; } public void Process(PipelineArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(Restarter, "Restarter"); Restarter.Restart(); } } }
Through a pipeline processor configuration setting, we define the type of IRestarter — it’s magically created by Sitecore when the pipeline processor instance is created.
After some null checks, the Process() method invokes Restart() on the IRestarter instance, ultimately restarting whatever the IRestarter is set to restart.
I then needed a way to test the pipelines I define later on in this post. I built the following class to serve as commands that I added into the Sitecore ribbon:
using Sitecore.Diagnostics; using Sitecore.Pipelines; using Sitecore.Shell.Framework.Commands; namespace Sitecore.Sandbox.Commands.Admin { public class Restart : Command { public override void Execute(CommandContext context) { Assert.ArgumentNotNull(context, "context"); Assert.ArgumentNotNull(context.Parameters, "context.Parameters"); Assert.ArgumentNotNullOrEmpty(context.Parameters["pipeline"], "context.Parameters[\"pipeline\"]"); CorePipeline.Run(context.Parameters["pipeline"], new PipelineArgs()); } } }
The command above expects a pipeline name to be supplied via the Parameters NameValueCollection instance set on the CommandContext instance passed to the Execute method() — I show later on in this post how I pass the name of the pipeline to the command.
If the pipeline name is given, we invoke the pipeline.
I then glued everything together using the following configuration file:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <commands> <command name="admin:Restart" type="Sitecore.Sandbox.Commands.Admin.Restart, Sitecore.Sandbox"/> </commands> <pipelines> <restartClient> <processor type="Sitecore.Sandbox.Pipelines.RestartRestarter.RestartRestarterOperations, Sitecore.Sandbox"> <Restarter type="Sitecore.Sandbox.Utilities.Restarters.SitecoreClientRestarter, Sitecore.Sandbox"/> </processor> </restartClient> <restartServer> <processor type="Sitecore.Sandbox.Pipelines.RestartRestarter.RestartRestarterOperations, Sitecore.Sandbox"> <Restarter type="Sitecore.Sandbox.Utilities.Restarters.SitecoreServerRestarter, Sitecore.Sandbox"/> </processor> </restartServer> </pipelines> </sitecore> </configuration>
I’m omitting how I created custom buttons in a custom ribbon in Sitecore to test this. If you want to learn about adding buttons to the Sitecore Ribbon, please read John West’s blog post on doing so.
However, I did do the following to pass the name of the pipeline we want to invoke in the custom command class defined above:
I wish I could show you some screenshots on how this works. However, there really isn’t much visual to see here.
If you have any suggestions on how I could show this in action, or improve the code above, please share in a comment.
Perform a Virus Scan on Files Uploaded into Sitecore
Last week I took notice of a SDN forum post asking whether there are any best practices for checking files uploaded into Sitecore for viruses.
Although I am unaware of there being any best practices for this, adding a processor into the <uiUpload> pipeline to perform virus scans has been on my plate for the past month. Not being able to find an open source .NET antivirus library has been my major blocker for building one.
However, after many hours of searching the web — yes, I do admit some of this time is sprinkled with idle surfing — over multiple weeks, I finally discovered nClam — a .NET client library for scanning files using a Clam Antivirus server (I setup one using instructions enumerated here).
Before I continue, I would like to caution you on using this or any antivirus solution before doing a substantial amount of research — I do not know how robust the solution I am sharing actually is, and I am by no means an antivirus expert. The purpose of this post is to show how one might go about adding antivirus capabilities into Sitecore, and I am not advocating or recommending any particular antivirus software package. Please consult with an antivirus expert before using any antivirus software/.NET client library .
The solution I came up with uses the adapter design pattern — it basically wraps and makes calls to the nClam library, and is accessible through the following antivirus scanner interface:
using System.IO; namespace Sitecore.Sandbox.Security.Antivirus { public interface IScanner { ScanResult Scan(Stream sourceStream); ScanResult Scan(string filePath); } }
This interface defines the bare minimum methods our antivirus classes should have. Instances of these classes should offer the ability to scan a file through the file’s Stream, or scan a file located on the server via the supplied file path.
The two scan methods defined by the interface must return an instance of the following data transfer object:
namespace Sitecore.Sandbox.Security.Antivirus { public class ScanResult { public string Message { get; set; } public ScanResultType ResultType { get; set; } } }
Instances of the above class contain a detailed message coupled with a ScanResultType enumeration value which conveys whether the scanned file is clean, contains a virus, or something else went wrong during the scanning process:
namespace Sitecore.Sandbox.Security.Antivirus { public enum ScanResultType { Clean, VirusDetected, Error, Unknown } }
I used the ClamScanResults enumeration as a model for the above.
I created and used the ScanResultType enumeration instead of the ClamScanResults enumeration so that this solution can be extended for other antivirus libraries — or calls could be made to other antivirus software through the command-line — and these shouldn’t be tightly coupled to the nClam library.
I then wrapped the nClam library calls in the following ClamScanner class:
using System; using System.IO; using System.Linq; using Sitecore.Diagnostics; using nClam; namespace Sitecore.Sandbox.Security.Antivirus { public class ClamScanner : IScanner { private ClamClient Client { get; set; } public ClamScanner(string server, string port) : this(server, int.Parse(port)) { } public ClamScanner(string server, int port) : this(new ClamClient(server, port)) { } public ClamScanner(ClamClient client) { SetClient(client); } private void SetClient(ClamClient client) { Assert.ArgumentNotNull(client, "client"); Client = client; } public ScanResult Scan(Stream sourceStream) { ScanResult result; try { result = CreateNewScanResult(Client.SendAndScanFile(sourceStream)); } catch (Exception ex) { result = CreateNewExceptionScanResult(ex); } return result; } public ScanResult Scan(string filePath) { ScanResult result; try { result = CreateNewScanResult(Client.SendAndScanFile(filePath)); } catch (Exception ex) { result = CreateNewExceptionScanResult(ex); } return result; } private static ScanResult CreateNewScanResult(ClamScanResult result) { Assert.ArgumentNotNull(result, "result"); if (result.Result == ClamScanResults.Clean) { return CreateNewScanResult("Yay! No Virus found!", ScanResultType.Clean); } if (result.Result == ClamScanResults.VirusDetected) { string message = string.Format("Oh no! The {0} virus was found!", result.InfectedFiles.First().VirusName); return CreateNewScanResult(message, ScanResultType.VirusDetected); } if (result.Result == ClamScanResults.Error) { string message = string.Format("Something went terribly wrong somewhere. Details: {0}", result.RawResult); return CreateNewScanResult(message, ScanResultType.Error); } return CreateNewScanResult("I have no clue about what just happened.", ScanResultType.Unknown); } private static ScanResult CreateNewExceptionScanResult(Exception ex) { string message = string.Format("Something went terribly wrong somewhere. Details:\n{0}", ex.ToString()); return CreateNewScanResult(ex.ToString(), ScanResultType.Error); } private static ScanResult CreateNewScanResult(string message, ScanResultType type) { return new ScanResult { Message = message, ResultType = type }; } } }
Methods in the above class make calls to the nClam library, and return a ScanResult intance containing detailed information about the file scan.
Next I developed the following <uiUpload> pipeline processor to use instances of classes that define the IScanner interface above, and these instances are set via Sitecore when instantiating this pipeline processor — the ClamScanner type is defined in the patch configuration file shown later in this post:
using System.IO; using System.Web; using Sitecore.Diagnostics; using Sitecore.Globalization; using Sitecore.Pipelines.Upload; using Sitecore.Sandbox.Security.Antivirus; namespace Sitecore.Sandbox.Pipelines.Upload { public class ScanForViruses { public void Process(UploadArgs args) { Assert.ArgumentNotNull(args, "args"); foreach (string fileKey in args.Files) { HttpPostedFile file = args.Files[fileKey]; ScanResult result = ScanFile(file.InputStream); if(result.ResultType != ScanResultType.Clean) { args.ErrorText = Translate.Text(string.Format("The file \"{0}\" cannot be uploaded. Reason: {1}", file.FileName, result.Message)); Log.Warn(args.ErrorText, this); args.AbortPipeline(); } } } private ScanResult ScanFile(Stream fileStream) { Assert.ArgumentNotNull(fileStream, "fileStream"); return Scanner.Scan(fileStream); } private IScanner Scanner { get; set; } } }
Uploaded files are passed to the IScanner instance, and the pipeline is aborted if something isn’t quite right — this occurs when a virus is detected, or an error is reported by the IScanner instance. If a virus is discovered, or an error occurs, the message contained within the ScanResult instance is captured in the Sitecore log.
I then glued everything together using the following patch configuration file:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <processors> <uiUpload> <processor mode="on" patch:before="processor[@type='Sitecore.Pipelines.Upload.CheckSize, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.Upload.ScanForViruses, Sitecore.Sandbox"> <Scanner type="Sitecore.Sandbox.Security.Antivirus.ClamScanner"> <param desc="server">localhost</param> <param desc="port">3310</param> </Scanner> </processor> </uiUpload> </processors> </sitecore> </configuration>
I wish I could show you this in action when a virus is discovered in an uploaded file. However, I cannot put my system at risk by testing with an infected file.
But, I can show you what happens when an error is detected. I will do this by shutting down the Clam Antivirus server on my machine:
On upload, I see the following:
When I opened up the Sitecore log, I see what the problem is:
Let’s turn it back on:
I can now upload files again:
If you have any suggestions on making this better, or know of a way to test it with a virus, please share in a comment below.
Choose Template Fields to Display in the Sitecore Content Editor
The other day I was going through search terms people had used to get to my blog, and discovered a few people made their way to my blog by searching for ‘sitecore hide sections in data template’.
I had built something like this in the past, but no longer remember how I had implemented that particular solution — not that I could show you that solution since it’s owned by a previous employer — and decided I would build another solution to accomplish this.
Before I move forward, I would like to point out that Sitecore MVP Andy Uzick wrote a blog post recently showing how to hide fields and sections in the content editor, though I did not have much luck with hiding sections in the way that he had done it — sections with no fields were still displaying for me in the content editor — and I decided to build a different solution altogether to make this work.
I thought it would be a good idea to let users turn this functionality on and off via a checkbox in the ribbon, and used some code from a previous post — in this post I had build an object to keep track of the state of a checkbox in the ribbon — as a model. In the spirit of that object, I defined the following interface:
namespace Sitecore.Sandbox.Utilities.ClientSettings { public interface IRegistrySettingToggle { bool IsOn(); void TurnOn(); void TurnOff(); } }
I then created the following abstract class which implements the interface above, and stores the state of the setting defined by the given key — the key of the setting and the “on” value are passed to it via a subclass — in the Sitecore registry.
using Sitecore.Diagnostics; using Sitecore.Web.UI.HtmlControls; namespace Sitecore.Sandbox.Utilities.ClientSettings { public abstract class RegistrySettingToggle : IRegistrySettingToggle { private string RegistrySettingKey { get; set; } private string RegistrySettingOnValue { get; set; } protected RegistrySettingToggle(string registrySettingKey, string registrySettingOnValue) { SetRegistrySettingKey(registrySettingKey); SetRegistrySettingOnValue(registrySettingOnValue); } private void SetRegistrySettingKey(string registrySettingKey) { Assert.ArgumentNotNullOrEmpty(registrySettingKey, "registrySettingKey"); RegistrySettingKey = registrySettingKey; } private void SetRegistrySettingOnValue(string registrySettingOnValue) { Assert.ArgumentNotNullOrEmpty(registrySettingOnValue, "registrySettingOnValue"); RegistrySettingOnValue = registrySettingOnValue; } public bool IsOn() { return Registry.GetString(RegistrySettingKey) == RegistrySettingOnValue; } public void TurnOn() { Registry.SetString(RegistrySettingKey, RegistrySettingOnValue); } public void TurnOff() { Registry.SetString(RegistrySettingKey, string.Empty); } } }
I then built the following class to toggle the display settings for our displayable fields:
using System; namespace Sitecore.Sandbox.Utilities.ClientSettings { public class ShowDisplayableFieldsOnly : RegistrySettingToggle { private const string RegistrySettingKey = "/Current_User/Content Editor/Show Displayable Fields Only"; private const string RegistrySettingOnValue = "on"; private static volatile IRegistrySettingToggle current; private static object lockObject = new Object(); public static IRegistrySettingToggle Current { get { if (current == null) { lock (lockObject) { if (current == null) { current = new ShowDisplayableFieldsOnly(); } } } return current; } } private ShowDisplayableFieldsOnly() : base(RegistrySettingKey, RegistrySettingOnValue) { } } }
It passes its Sitecore registry key and “on” state value to the RegistrySettingToggle base class, and employs the Singleton pattern — I saw no reason for there to be multiple instances of this object floating around.
In order to use a checkbox in the ribbon, we have to create a new command for it:
using System.Linq; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Shell.Framework.Commands; using Sitecore.Sandbox.Utilities.ClientSettings; namespace Sitecore.Sandbox.Commands { public class ToggleDisplayableFieldsVisibility : Command { public override void Execute(CommandContext context) { Assert.ArgumentNotNull(context, "context"); ToggleDisplayableFields(); Refresh(context); } private static void ToggleDisplayableFields() { IRegistrySettingToggle showDisplayableFieldsOnly = ShowDisplayableFieldsOnly.Current; if (!showDisplayableFieldsOnly.IsOn()) { showDisplayableFieldsOnly.TurnOn(); } else { showDisplayableFieldsOnly.TurnOff(); } } public override CommandState QueryState(CommandContext context) { if (!ShowDisplayableFieldsOnly.Current.IsOn()) { return CommandState.Enabled; } return CommandState.Down; } private static void Refresh(CommandContext context) { Refresh(GetItem(context)); } private static void Refresh(Item item) { Assert.ArgumentNotNull(item, "item"); Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", item.ID), 1); } private static Item GetItem(CommandContext context) { Assert.ArgumentNotNull(context, "context"); return context.Items.FirstOrDefault(); } } }
The command above leverages the instance of the ShowDisplayableFieldsOnly class defined above to turn the displayable fields feature on and off.
I followed the creation of the command above with the definition of the ribbon checkbox in the core database:
The command name above — which is set in the Click field — is defined in the patch configuration file towards the end of this post.
I then created the following data template with a TreelistEx field to store the displayable fields:
The TreelistEx field above will pull in all sections and their fields into the TreelistEx dialog, but only allow the selection of template fields, as is dictated by the following parameters that I have mapped in its Source field:
DataSource=/sitecore/templates/Sample/Sample Item&IncludeTemplatesForSelection=Template field&IncludeTemplatesForDisplay=Template section,Template field&AllowMultipleSelection=no
I then set this as a base template in my sandbox solution’s Sample Item template:
In order to remove fields, we need a <getContentEditorFields> pipeline processor. I built the following class for to serve as one:
using System; using Sitecore.Configuration; using Sitecore.Data.Fields; using Sitecore.Data.Items; using Sitecore.Data.Managers; using Sitecore.Data.Templates; using Sitecore.Diagnostics; using Sitecore.Shell.Applications.ContentManager; using Sitecore.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields; using Sitecore.Sandbox.Utilities.ClientSettings; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields { public class RemoveUndisplayableFields { public void Process(GetContentEditorFieldsArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Item, "args.Item"); Assert.ArgumentCondition(!string.IsNullOrWhiteSpace(DisplayableFieldsFieldName), "DisplayableFieldsFieldName", "DisplayableFieldsFieldName must be set in the configuration file!"); if (!ShowDisplayableFieldsOnly.Current.IsOn()) { return; } foreach (Editor.Section section in args.Sections) { AddDisplayableFields(args.Item[DisplayableFieldsFieldName], section); } } private void AddDisplayableFields(string displayableFieldIds, Editor.Section section) { Editor.Fields displayableFields = new Editor.Fields(); foreach (Editor.Field field in section.Fields) { if (IsDisplayableField(displayableFieldIds, field)) { displayableFields.Add(field); } } section.Fields.Clear(); section.Fields.AddRange(displayableFields); } private bool IsDisplayableField(string displayableFieldIds, Editor.Field field) { if (IsStandardValues(field.ItemField.Item)) { return true; } if (IsDisplayableFieldsField(field.ItemField)) { return false; } return IsStandardTemplateField(field.ItemField) || string.IsNullOrWhiteSpace(displayableFieldIds) || displayableFieldIds.Contains(field.ItemField.ID.ToString()); } private bool IsDisplayableFieldsField(Field field) { return string.Equals(field.Name, DisplayableFieldsFieldName, StringComparison.CurrentCultureIgnoreCase); } private static bool IsStandardValues(Item item) { if (item.Template.StandardValues != null) { return item.Template.StandardValues.ID == item.ID; } return false; } private bool IsStandardTemplateField(Field field) { Assert.IsNotNull(StandardTemplate, "The Stardard Template could not be found."); return StandardTemplate.ContainsField(field.ID); } private static Template GetStandardTemplate() { return TemplateManager.GetTemplate(Settings.DefaultBaseTemplate, Context.ContentDatabase); } private Template _StandardTemplate; private Template StandardTemplate { get { if (_StandardTemplate == null) { _StandardTemplate = GetStandardTemplate(); } return _StandardTemplate; } } private string DisplayableFieldsFieldName { get; set; } } }
The class above iterates over all fields for the supplied item, and adds only those that were selected in the Displayable Fields TreelistEx field, and also Standard Fields — we don’t want to remove these since they are shown/hidden by the Standard Fields feature in Sitecore — to a new Editor.Fields collection. This new collection is then set on the GetContentEditorFieldsArgs instance.
Plus, we don’t want to show the Displayable Fields TreelistEx field when the feature is turned on, and we are on an item. This field should only display when we are on the standard values item when the feature is turned on — this is how we will choose our displayable fields.
Now we have to handle sections without fields — especially after ripping them out via the pipeline processor above.
I built the following class to serve as a <renderContentEditor> pipeline processor to do this:
using System.Linq; using Sitecore.Diagnostics; using Sitecore.Shell.Applications.ContentManager; using Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor { public class FilterSectionsWithFields { public void Process(RenderContentEditorArgs args) { Assert.ArgumentNotNull(args, "args"); args.Sections = GetSectionsWithFields(args.Sections); } private static Editor.Sections GetSectionsWithFields(Editor.Sections sections) { Assert.ArgumentNotNull(sections, "sections"); Editor.Sections sectionsWithFields = new Editor.Sections(); foreach (Editor.Section section in sections) { AddIfContainsFields(sectionsWithFields, section); } return sectionsWithFields; } private static void AddIfContainsFields(Editor.Sections sections, Editor.Section section) { Assert.ArgumentNotNull(sections, "sections"); Assert.ArgumentNotNull(section, "section"); if (!ContainsFields(section)) { return; } sections.Add(section); } private static bool ContainsFields(Editor.Section section) { Assert.ArgumentNotNull(section, "section"); return section.Fields != null && section.Fields.Any(); } } }
It basically builds a new collection of sections that contain at least one field, and sets it on the RenderContentEditorArgs instance being passed through the <renderContentEditor> pipeline.
In order for this to work, we must make this pipeline processor run before all other processors.
I tied all of the above together with the following patch configuration file:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <commands> <command name="contenteditor:ToggleDisplayableFieldsVisibility" type="Sitecore.Sandbox.Commands.ToggleDisplayableFieldsVisibility, Sitecore.Sandbox"/> </commands> <pipelines> <getContentEditorFields> <processor type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields.RemoveUndisplayableFields, Sitecore.Sandbox"> <DisplayableFieldsFieldName>Displayable Fields</DisplayableFieldsFieldName> </processor> </getContentEditorFields> <renderContentEditor> <processor patch:before="processor[@type='Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderSkinedContentEditor, Sitecore.Client']" type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.FilterSectionsWithFields, Sitecore.Sandbox"/> </renderContentEditor> </pipelines> </sitecore> </configuration>
Let’s try this out.
I navigated to the standard values item for my Sample Item data template, and selected the fields I want to display in the content editor:
I then went to an item that uses this data template, and turned on the displayable fields feature:
As you can see, only the fields we had chosen display — along with standard fields since the Standard Fields checkbox is checked.
I then turned off the displayable fields feature:
Now all fields for the item display.
I then turned the displayable fields feature back on, and turned off Standard Fields:
Now only our selected fields display.
If you have any thoughts on this, or ideas around making this better, please share in a comment.
Resolve Media Library Items Linked in Sitecore Aliases
Tonight I was doing research on extending the aliases feature in Sitecore, and discovered media library items linked in them are not served correctly “out of the box”:
As an enhancement, I wrote the following HttpRequestProcessor subclass to be used in the httpRequestBegin pipeline:
using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Pipelines.HttpRequest; using Sitecore.Resources.Media; using Sitecore.Web; namespace Sitecore.Sandbox.Pipelines.HttpRequest { public class MediaAliasResolver : HttpRequestProcessor { public override void Process(HttpRequestArgs args) { Assert.ArgumentNotNull(args, "args"); if (!CanProcessAliases()) { return; } string mediaUrl = GetMediaAliasTargetUrl(args); if (string.IsNullOrWhiteSpace(mediaUrl)) { return; } Context.Page.FilePath = mediaUrl; } private static bool CanProcessAliases() { return Settings.AliasesActive && Context.Database != null; } private static string GetMediaAliasTargetUrl(HttpRequestArgs args) { Assert.ArgumentNotNull(args, "args"); ID targetID = Context.Database.Aliases.GetTargetID(args.LocalPath); if (targetID.IsNull) { return string.Empty; } Item targetItem = args.GetItem(targetID); if (targetItem == null || !IsMediaItem(targetItem)) { return string.Empty; } return GetAbsoluteMediaUrl(targetItem); } private static bool IsMediaItem(Item item) { Assert.ArgumentNotNull(item, "item"); return item.Paths.IsMediaItem && item.TemplateID != TemplateIDs.MediaFolder; } private static string GetAbsoluteMediaUrl(MediaItem mediaItem) { string relativeUrl = MediaManager.GetMediaUrl(mediaItem); return WebUtil.GetFullUrl(relativeUrl); } } }
The HttpRequestProcessor subclass above — after ascertaining the aliases feature is turned on, and the item linked in the requested alias is a media library item — gets the absolute URL for the media library item, and sets it on the FilePath property of the Sitecore.Context.Page instance — this is exactly how the “out of the box” Sitecore.Pipelines.HttpRequest.AliasResolver handles external URLs — and passes along the HttpRequestArgs instance.
I then wedged the HttpRequestProcessor subclass above into the httpRequestBegin pipeline directly before the Sitecore.Pipelines.HttpRequest.AliasResolver:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <httpRequestBegin> <processor patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.AliasResolver, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.HttpRequest.MediaAliasResolver, Sitecore.Sandbox" /> </httpRequestBegin> </pipelines> </sitecore> </configuration>
Let’s take this for a spin.
I had already defined the following alias in Sitecore beforehand — the error page at the top of this post is evidence of that:
After navigating to http://sandbox/pizza — the URL to the pizza alias in my local Sitecore sandbox instance (don’t click on this link because it won’t go anywhere unless you have a website named sandbox running on your local machine) — I was brought to the media library image on the front-end:
If you have any recommendations on improving this, or further thoughts on using aliases in Sitecore, please share in a comment below.
Until next time, have a pizzalicious day!
Specify the Maximum Width of Images Uploaded to the Sitecore Media Library
Last week someone started a thread in one of the SDN forums asking how one could go about making Sitecore resize images larger than a specific width down to that width.
Yesterday an astute SDN visitor recommended using a custom getMediaStream pipeline processor to set the maximum width for images — a property for this maximum width is exposed via the GetMediaStreamPipelineArgs parameters object passed through the getMediaStream pipeline.
I thought I would try out the suggestion, and came up with the following getMediaStream pipeline processor:
using Sitecore.Diagnostics; using Sitecore.Resources.Media; namespace Sitecore.Sandbox.Resources.Media { public class MaxWidthProcessor { public int MaxWidth { get; set; } public void Process(GetMediaStreamPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); if (!ShouldSetMaxWidth(args)) { return; } args.Options.MaxWidth = MaxWidth; } private bool ShouldSetMaxWidth(GetMediaStreamPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); return MaxWidth > 0 && args.Options.MaxWidth < 1; } } }
I then interlaced it into the getMediaStream pipeline before the ResizeProcessor processor — this is the processor where the magical resizing happens — using the following patch configuration file:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <getMediaStream> <processor patch:before="processor[@type='Sitecore.Resources.Media.ResizeProcessor, Sitecore.Kernel']" type="Sitecore.Sandbox.Resources.Media.MaxWidthProcessor, Sitecore.Sandbox"> <MaxWidth>1024</MaxWidth> </processor> </getMediaStream> </pipelines> </sitecore> </configuration>
The maximum width for images is set to 1024 pixels — the width I am using in my test below.
Let’s see how we did.
I decided to use one of my favorite images that ships with Sitecore for testing:
As you can see its width is much larger than 1024 pixels:
After uploading the lighthouse image into the media library, its width was set to the maximum specified, and its height was scaled down proportionally:
If you have any thoughts on this, or other ideas on resizing images uploaded to the media library, please drop a comment.
Specify Which Sitecore Web Forms for Marketers Form To Render Via the Query String
Today I saw a post in one of the SDN forums asking how one could go about building a page in Sitecore that can render a Web Forms for Marketers (WFFM) form based on an ID passed via the query string.
I built the following WFFM FormRenderer as a “proof of concept” to accomplish this — this solution assumes the ID we are passing is the ID of the form, and not some other ID (or guid):
using System; using System.Web; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Form.Core.Configuration; using Sitecore.Form.Core.Renderings; namespace Sitecore.Sandbox.Form.Core.Renderings { public class DetectIDFormRenderer : FormRender { protected override void OnInit(System.EventArgs e) { string detectedFormId = GetDetectedFormId(); if (IsValidFormId(detectedFormId)) { FormID = detectedFormId; } base.OnInit(e); } private static string GetDetectedFormId() { return HttpContext.Current.Request["formId"]; } private static bool IsValidFormId(string id) { return !string.IsNullOrWhiteSpace(id) && IsID(id) && IsFormId(id); } private static bool IsID(string id) { Sitecore.Data.ID sitecoreID; return Sitecore.Data.ID.TryParse(id, out sitecoreID); } private static bool IsFormId(string id) { Item item = StaticSettings.ContextDatabase.GetItem(id); return item != null && item.TemplateID == IDs.FormTemplateID; } } }
The FormRenderer above grabs the specified form’s ID via a query string parameter, ascertains whether it’s truly an ID, and determines whether it is an ID of a WFFM Form in Sitecore — these are done via the IsID and IsFormId methods.
If the supplied form ID is valid, we save it to the FormID property defined in the base FormerRender class. Otherwise, we flow through to the “out of the box” logic.
Now it’s time to register the above class in Sitecore.
I duplicated the “out of the box” Form Webcontrol under /sitecore/layout/Renderings/Modules/Web Forms for Marketers, renamed the item to something appropriate, and updated the code-defining fields to point to our new FormRender above:
I decided to reuse an existing page item with a WFFM form — I didn’t want to step through ‘Insert Form’ wizard so that I could save time — and swapped out the “out of the box” Form Webcontrol with the new one we created above:
I ensured we had a default form set just in case of query string manipulation, or in the event the form cannot be found by the given ID:
I published everything, and navigated to my form page:
I then specified the empty guid:
I manipulated the query string again, but this time passing a valid form ID:
I then changed the form ID again but with another valid form ID:
If you have any suggestions around making this better, or ideas for a different solution, please drop a comment.
Restrict Certain Types of Files From Being Uploaded in Sitecore
Tonight I was struck by a thought while conducting research for a future blog post: should we prevent users from uploading certain types of files in Sitecore for security purposes?
You might thinking “Prevent users from uploading files? Mike, what on Earth are you talking about?”
What I’m suggesting is we might want to consider restricting certain types of files — specifically executables (these have an extension of .exe) and DOS command files (these have an extension of .com) — from being uploaded into Sitecore, especially when files can be easily downloaded from the media library.
Why should we do this?
Doing this will curtail the probability of viruses being spread among our Sitecore users — such could happen if one user uploads an executable that harbors a virus, and other users of our Sitecore system download that tainted executable, and run it on their machines.
As a “proof of concept”, I built the following uiUpload pipeline processor — the uiUpload pipeline lives in /configuration/sitecore/processors/uiUpload in your Sitecore instance’s Web.config — to restrict certain types of files from being uploaded into Sitecore:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; using System.Xml; using Sitecore.Diagnostics; using Sitecore.Globalization; using Sitecore.Pipelines.Upload; namespace Sitecore.Sandbox.Pipelines.Upload { public class CheckForRestrictedFiles : UploadProcessor { private List<string> _RestrictedExtensions; private List<string> RestrictedExtensions { get { if (_RestrictedExtensions == null) { _RestrictedExtensions = new List<string>(); } return _RestrictedExtensions; } } public void Process(UploadArgs args) { foreach(string fileKey in args.Files) { string fileName = GetFileName(args.Files, fileKey); string extension = Path.GetExtension(fileName); if (IsRestrictedExtension(extension)) { args.ErrorText = Translate.Text(string.Format("The file \"{0}\" cannot be uploaded. Files with an extension of {1} are not allowed.", fileName, extension)); Log.Warn(args.ErrorText, this); args.AbortPipeline(); } } } private static string GetFileName(HttpFileCollection files, string fileKey) { Assert.ArgumentNotNull(files, "files"); Assert.ArgumentNotNullOrEmpty(fileKey, "fileKey"); return files[fileKey].FileName; } private bool IsRestrictedExtension(string extension) { return RestrictedExtensions.Exists(restrictedExtension => string.Equals(restrictedExtension, extension, StringComparison.CurrentCultureIgnoreCase)); } protected virtual void AddRestrictedExtension(XmlNode configNode) { if (configNode == null || string.IsNullOrWhiteSpace(configNode.InnerText)) { return; } RestrictedExtensions.Add(configNode.InnerText); } } }
The class above ascertains whether each uploaded file has an extension that is restricted — restricted extensions are defined in the configuration file below, and are added to a list of strings via the AddRestrictedExtensions method — and logs an error message when a file possessing a restricted extension is encountered.
I then tied everything together using the following patch configuration file including specifying some file extensions to restrict:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <processors> <uiUpload> <processor mode="on" type="Sitecore.Sandbox.Pipelines.Upload.CheckForRestrictedFiles, Sitecore.Sandbox" patch:before="processor[@type='Sitecore.Pipelines.Upload.CheckSize, Sitecore.Kernel']"> <restrictedExtensions hint="raw:AddRestrictedExtension"> <!-- Be sure to prefix with a dot --> <extension>.exe</extension> <extension>.com</extension> </restrictedExtensions> </processor> </uiUpload> </processors> </sitecore> </configuration>
Let’s try this out.
I went to the media library, and attempted to upload an executable:
After clicking the “Open” button, I was presented with the following:
An error in my Sitecore instance’s latest log file conveys why I could not upload the chosen file:
If you have thoughts on this, or have ideas for other processors that should be added to uiUpload pipeline, please share in a comment.
Periodically Unlock Items of Idle Users in Sitecore
In my last post I showed a way to unlock locked items of a user when he/she logs out of Sitecore.
I wrote that article to help out the poster of this thread in one of the forums on SDN.
In response to my reply in that thread — I had linked to my previous post in that reply — John West, Chief Technology Officer of Sitecore, had asked whether we would also want to unlock items of users whose sessions had expired, and another SDN user had alluded to the fact that my solution would not unlock items for users who had closed their browser sessions instead of explicitly logging out of Sitecore.
Immediate after reading these responses, I began thinking about a supplemental solution to unlock items for “idle” users — users who have not triggered any sort of request in Sitecore after a certain amount of time.
I first began tinkering with the idea of using the last activity date/time of the logged in user — this is available as a DateTime in the user’s MembershipUser instance via the LastActivityDate property.
However — after reading this article — I learned this date and time does not mean what I thought it had meant, and decided to search for another way to ascertain whether a user is idle in Sitecore.
After some digging around, I discovered Sitecore.Web.Authentication.DomainAccessGuard.Sessions in Sitecore.Kernel.dll — this appears to be a collection of sessions in Sitecore — and immediately felt elated as if I had just won the lottery. I decided to put it to use in the following class (code in this class will be invoked via a scheduled task in Sitecore):
using System; using System.Collections.Generic; using System.Web.Security; using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Security.Accounts; using Sitecore.Tasks; using Sitecore.Web.Authentication; namespace Sitecore.Sandbox.Tasks { public class UnlockItemsTask { private static readonly TimeSpan ElapsedTimeWhenIdle = GetElapsedTimeWhenIdle(); public void UnlockIdleUserItems(Item[] items, CommandItem command, ScheduleItem schedule) { if (ElapsedTimeWhenIdle == TimeSpan.Zero) { return; } IEnumerable<Item> lockedItems = GetLockedItems(schedule.Database); foreach (Item lockedItem in lockedItems) { UnlockIfApplicable(lockedItem); } } private static IEnumerable<Item> GetLockedItems(Database database) { Assert.ArgumentNotNull(database, "database"); return database.SelectItems("fast://*[@__lock='%owner=%']"); } private void UnlockIfApplicable(Item item) { Assert.ArgumentNotNull(item, "item"); if (!ShouldUnlockItem(item)) { return; } Unlock(item); } private static bool ShouldUnlockItem(Item item) { Assert.ArgumentNotNull(item, "item"); if(!item.Locking.IsLocked()) { return false; } string owner = item.Locking.GetOwner(); return !IsUserAdmin(owner) && IsUserIdle(owner); } private static bool IsUserAdmin(string username) { Assert.ArgumentNotNullOrEmpty(username, "username"); User user = User.FromName(username, false); Assert.IsNotNull(user, "User must be null due to a wrinkle in the interwebs :-/"); return user.IsAdministrator; } private static bool IsUserIdle(string username) { Assert.ArgumentNotNullOrEmpty(username, "username"); DomainAccessGuard.Session userSession = DomainAccessGuard.Sessions.Find(session => session.UserName == username); if(userSession == null) { return true; } return userSession.LastRequest.Add(ElapsedTimeWhenIdle) <= DateTime.Now; } private void Unlock(Item item) { Assert.ArgumentNotNull(item, "item"); try { string owner = item.Locking.GetOwner(); item.Editing.BeginEdit(); item.Locking.Unlock(); item.Editing.EndEdit(); Log.Info(string.Format("Unlocked {0} - was locked by {1}", item.Paths.Path, owner), this); } catch (Exception ex) { Log.Error(this.ToString(), ex, this); } } private static TimeSpan GetElapsedTimeWhenIdle() { TimeSpan elapsedTimeWhenIdle; if (TimeSpan.TryParse(Settings.GetSetting("UnlockItems.ElapsedTimeWhenIdle"), out elapsedTimeWhenIdle)) { return elapsedTimeWhenIdle; } return TimeSpan.Zero; } } }
Methods in the class above grab all locked items in Sitecore via a fast query, and unlock them if the users of each are not administrators, and are idle — I determine this from an idle threshold value that is stored in a custom setting (see the patch configuration file below) and the last time the user had made any sort of request in Sitecore via his/her DomainAccessGuard.Session instance from Sitecore.Web.Authentication.DomainAccessGuard.Sessions.
If a DomainAccessGuard.Session instance does not exist for the user — it’s null — this means the user’s session had expired, so we should also unlock the item.
I’ve also included code to log which items have been unlocked by the Unlock method — for auditing purposes — and of course log exceptions if any are encountered — we must do all we can to support our support teams by capturing information in log files :).
I then created a patch configuration file to store our idle threshold value — I’ve used one minute here for testing (I can’t sit around all day waiting for items to unlock ;)):
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <settings> <setting name="UnlockItems.ElapsedTimeWhenIdle" value="00:00:01:00" /> </settings> </sitecore> </configuration>
I then created a task command for the class above in Sitecore:
I then mapped the task command to a scheduled task (to learn about scheduled tasks, see John West’s blog post where he discusses them):
Let’s light the fuse on this, and see what it does.
I logged into Sitecore using one of my test accounts, and locked some items:
I then logged into Sitecore using a different account in a different browser session, and navigated to one of the locked items:
I then walked away, made a cup of coffee, returned, and saw this:
I opened up my latest Sitecore log, and saw the following:
I do want to caution you from running off with this code, and putting it into your Sitecore instance(s) — it is an all or nothing solution (it will unlock items for all non-adminstrators which might invoke some anger in users, and also defeat the purpose of locking items in the first place), so it’s quite important that a business decision is made before using this solution, or one that is similar in nature.
If you have any thoughts on this, please leave a comment.