Home » 2013

Yearly Archives: 2013

Advertisements

Delete All But This: Delete Sibling Items Using a Custom Item Context Menu Option in Sitecore

Every so often I find myself having to delete all Sitecore items in a folder except for one — the reason for this eludes me at the moment, but it does make an appearance once in a while (if this also happens to you, and you remember the context for why it happens, please share in a comment) — and it feels like it takes ages to delete all of these items: I have to step through all of these items in the Sitecore content tree, and delete each individually .

When this happens I usually say to myself “wouldn’t it be cool to have something like the ‘Close All But This’ feature found in Visual Studio?”:

close-all-but-this-vs

I always forget to write this idea down, but did remember it a couple of days ago, and decided to build something to save time when deleting all items in a folder except for one.

To delete all items in a folder, we need a way to get all sibling items, and exclude the item we don’t want to delete. I decided to create a custom pipeline to do this, and defined the following parameter object for it:

using System.Collections.Generic;
using System.Linq;

using Sitecore.Data.Items;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.GetSiblings
{
    public class GetSiblingsArgs : PipelineArgs
    {
        public Item Item { get; set; }

        public IEnumerable<Item> Siblings { get; set; } 
    }
}

Now that we have the parameter object defined, we need a class with methods that will compose our custom pipeline for grabbing sibling items in Sitecore. The following class does the trick:

using System.Linq;

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.GetSiblings
{
    public class GetSiblingsOperations
    {
        public void EnsureItem(GetSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (args.Item == null)
            {
                args.AbortPipeline();
            }
        }

        public void GetSiblings(GetSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Item, "args.Item");
            args.Siblings = (from sibling in args.Item.Parent.GetChildren()
                             where sibling.ID != args.Item.ID
                             select sibling).ToList();
        }
    }
}

The EnsureItem() method above just makes sure the item instance passed to it isn’t null, and aborts the pipeline if it is.

The GetSiblings() method gets all siblings items of the item — it just grabs all children of its parent, and excludes the item in question from the resulting collection using LINQ.

Now that we have a way to get sibling items, we need a way to delete them. I decided to build another custom pipeline to get sibling items for an item — by leveraging the pipeline created above — and delete them, and created the following parameter object for it:

using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings
{
    public class DeleteSiblingsArgs : ClientPipelineArgs
    {
        public Item Item { get; set; }

        public bool ShouldDelete { get; set; }

        public IEnumerable<Item> Siblings { get; set; } 
    }
}

The following class contains methods that will be used it our custom client pipeline to delete sibling items:

using System;
using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Pipelines.GetSiblings;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings
{
    public class DeleteSiblingsOperations
    {
        private string DeleteConfirmationMessage { get; set; }
        private string DeleteConfirmationWindowWidth { get; set; }
        private string DeleteConfirmationWindowHeight { get; set; }

        public void ConfirmDeleteAction(DeleteSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(DeleteConfirmationMessage, "DeleteConfirmationMessage");
            Assert.ArgumentNotNullOrEmpty(DeleteConfirmationWindowWidth, "DeleteConfirmationWindowWidth");
            Assert.ArgumentNotNullOrEmpty(DeleteConfirmationWindowHeight, "DeleteConfirmationWindowHeight");
            if (!args.IsPostBack)
            {
                SheerResponse.YesNoCancel(DeleteConfirmationMessage, DeleteConfirmationWindowWidth, DeleteConfirmationWindowHeight);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.ShouldDelete = AreEqualIgnoreCase(args.Result, "yes");
                args.IsPostBack = false;
            }
        }

        public void GetSiblingsIfConfirmed(DeleteSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!args.ShouldDelete)
            {
                args.AbortPipeline();
                return;
            }
            
            args.Siblings = GetSiblings(args.Item);
        }

        protected virtual IEnumerable<Item> GetSiblings(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            GetSiblingsArgs getSiblingsArgs = new GetSiblingsArgs { Item = item };
            CorePipeline.Run("getSiblings", getSiblingsArgs);
            return getSiblingsArgs.Siblings;
        }

        public void DeleteSiblings(DeleteSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Siblings, "args.Siblings");
            DeleteItems(args.Siblings);
        }

        protected virtual void DeleteItems(IEnumerable<Item> items)
        {
            Assert.ArgumentNotNull(items, "items");
            foreach (Item item in items)
            {
                item.Recycle();
            }
        }

        private static bool AreEqualIgnoreCase(string one, string two)
        {
            return string.Equals(one, two, StringComparison.CurrentCultureIgnoreCase);
        }
    }
}

The ConfirmDeleteAction() method — which is invoked first in the custom client pipeline — asks the user if he/she would like to delete all sibling items for the item in question.

The GetSiblingsIfConfirmed() method is then processed next in the pipeline sequence, and ascertains whether the user had clicked the ‘Yes’ button — this is a button in the YesNoCancel dialog that was presented to the user via the ConfirmDeleteAction() method.

If the user had clicked the ‘Yes’ button, sibling items are grabbed from Sitecore — this is done using the custom pipeline built above — and is set on Siblings property of the DeleteSiblingsArgs instance.

The DeleteSiblings() method is then processed next, and basically does as named: it deletes all items in the Siblings property of the DeleteSiblingsArgs instance.

Now that we are armed with our pipelines above, we need a way to call them from the Sitecore UI. The following command was built for that purpose:

using System.Collections.Generic;
using System.Linq;

using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Framework.Commands;

using Sitecore.Sandbox.Pipelines.GetSiblings;
using Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings;

namespace Sitecore.Sandbox.Shell.Framework.Commands
{
    public class DeleteAllButThis : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            DeleteSiblings(context);
        }

        private static void DeleteSiblings(CommandContext context)
        {
            Context.ClientPage.Start("uiDeleteSiblings", new DeleteSiblingsArgs { Item = GetItem(context) });
        }

        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            if (!HasSiblings(GetItem(context)))
            {
                return CommandState.Hidden;
            }

            return CommandState.Enabled;
        }

        private static bool HasSiblings(Item item)
        {
            return GetSiblings(item).Any();
        }

        private static IEnumerable<Item> GetSiblings(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            GetSiblingsArgs getSiblingsArgs = new GetSiblingsArgs { Item = item };
            CorePipeline.Run("getSiblings", getSiblingsArgs);
            return getSiblingsArgs.Siblings;
        }

        private static Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Items, "context.Items");
            return context.Items.FirstOrDefault();
        }
    }
}

The command above will only display if the context item has siblings — we invoke the pipeline defined towards the beginning of this post to get sibling items. If none are returned, the command is hidden.

When the command executes, we basically just pass the context item to our custom pipeline that deletes sibling items, and sit back and wait for them to be deleted (I just lean back, and put my feet up on my desk).

I then strung everything together using the following configuration include file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:DeleteAllButThis" type="Sitecore.Sandbox.Shell.Framework.Commands.DeleteAllButThis, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <getSiblings>
        <processor type="Sitecore.Sandbox.Pipelines.GetSiblings.GetSiblingsOperations, Sitecore.Sandbox" method="EnsureItem" />
        <processor type="Sitecore.Sandbox.Pipelines.GetSiblings.GetSiblingsOperations, Sitecore.Sandbox" method="GetSiblings" />
      </getSiblings>
    </pipelines>
    <processors>
      <uiDeleteSiblings>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings.DeleteSiblingsOperations, Sitecore.Sandbox" method="ConfirmDeleteAction">
          <DeleteConfirmationMessage>Are you sure you want to delete all sibling items and their descendants?</DeleteConfirmationMessage>
          <DeleteConfirmationWindowWidth>200</DeleteConfirmationWindowWidth>
          <DeleteConfirmationWindowHeight>200</DeleteConfirmationWindowHeight>
        </processor>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings.DeleteSiblingsOperations, Sitecore.Sandbox" method="GetSiblingsIfConfirmed"/>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings.DeleteSiblingsOperations, Sitecore.Sandbox" method="DeleteSiblings"/>
	    </uiDeleteSiblings>
    </processors>
  </sitecore>
</configuration>

I also had to wire the command above to the Sitecore UI by defining a context menu item in the core database. I’ve omitted how I’ve done this. If you would like to learn how to do this, check out my first and second posts on adding to the item context menu.

Let’s see this in action.

I first created some items to delete, and one item that I don’t want to delete:

stuff-to-delete

I then right-clicked on the item I don’t want to delete, and was presented with the new item context menu option:

delete-all-but-this-context-menu

I was then prompted with a confirmation dialog:

delete-all-but-this-confirmation-dialog

I clicked ‘Yes’, and then saw that all sibling items were deleted:

items-deleted

If you have any suggestions on making this better, please drop a comment.

Until next time, have a Sitecoretastic day!

Advertisements

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:

install-package-end-wizard

Until last week, I never understood how either of these checkboxes worked, and uncovered the following code during my research:

restart-code-in-InstallPackageForm

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:

restart-client-button

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:

scanner-turned-off

On upload, I see the following:

tried-to-upload-file

When I opened up the Sitecore log, I see what the problem is:

log-error

Let’s turn it back on:

scanner-turned-on

I can now upload files again:

upload-no-error

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:

show-displayable-fields-checkbox-defined

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:

displayable-fields-template

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:

set-displayable-fields-template-as-base

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:

selected-some-fields

I then went to an item that uses this data template, and turned on the displayable fields feature:

displayable-fields-on

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:

displayable-fields-off

Now all fields for the item display.

I then turned the displayable fields feature back on, and turned off Standard Fields:

standard-fields-off-displayable-fields-on

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”:

pizza-alias-no-workie

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:

pizza-alias

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:

media-alias-pizza-redirected

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!

Insert a New Item After a Sibling Item in Sitecore

Have you ever thought to yourself “wouldn’t it be nice to insert a new item in the Sitecore content tree at a specific place among its siblings without having to move the inserted item up or down multiple times to position it correctly?”

I’ve had this thought more than once, and decided to put something together to achieve this.

The following class consists of methods to be used in pipeline processors of the uiAddFromTemplate pipeline to make this happen:

using System.Collections.Generic;
using System.Linq;

using Sitecore.Configuration;
using Sitecore.Globalization;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.Dialogs.ItemLister;
using Sitecore.Shell.Framework;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate
{
    public class InsertAfterItemOperations
    {
        private string SelectButtonText { get; set; }

        private string ModalIcon { get; set; }

        private string ModalTitle { get; set; }

        private string ModalInstructions { get; set; }

        public void StoreTemplateResult(ClientPipelineArgs args)
        {
            args.Parameters["Template"] = args.Result;
        }

        public void EnsureParentAndChildren(ClientPipelineArgs args)
        {
            AssertArguments(args);
            Item parent = GetParentItem(args);
            EnsureParentItem(parent, args);
            args.Parameters["HasChildren"] = parent.HasChildren.ToString();
            args.IsPostBack = false;
        }

        public void GetInsertAfterId(ClientPipelineArgs args)
        {
            AssertArguments(args);
            bool hasChildren = false;
            bool.TryParse(args.Parameters["HasChildren"], out hasChildren);
            if (!hasChildren)
            {
                SetCanAddItemFromTemplate(args);
                return;
            }

            Item parent = GetParentItem(args);
            EnsureParentItem(parent, args);
            if (!args.IsPostBack)
            {
                ItemListerOptions itemListerOptions = new ItemListerOptions
                {
                    ButtonText = SelectButtonText,
                    Icon = ModalIcon,
                    Title = ModalTitle,
                    Text = ModalInstructions,
                    Items = parent.Children.ToList()
                };

                SheerResponse.ShowModalDialog(itemListerOptions.ToUrlString().ToString(), true);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["InsertAfterId"] = args.Result;
                SetCanAddItemFromTemplate(args);
                args.IsPostBack = false;
            }
            else
            {
                SetCanAddItemFromTemplate(args);
                args.IsPostBack = false;
            }
        }

        private void SetCanAddItemFromTemplate(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.Parameters["CanAddItemFromTemplate"] = bool.TrueString;
        }

        private static Item GetParentItem(ClientPipelineArgs args)
        {
            AssertArguments(args);
            Assert.ArgumentNotNullOrEmpty(args.Parameters["id"], "id");
            return GetItem(GetDatabase(args.Parameters["database"]), args.Parameters["id"], args.Parameters["language"]);
        }

        private static void EnsureParentItem(Item parent, ClientPipelineArgs args)
        {
            if (parent != null)
            {
                return;
            }

            SheerResponse.Alert("Parent item could not be located -- perhaps it was deleted.");
            args.AbortPipeline();
        }

        public void AddItemFromTemplate(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            bool canAddItemFromTemplate = false;
            bool.TryParse(args.Parameters["CanAddItemFromTemplate"], out canAddItemFromTemplate);
            if (canAddItemFromTemplate)
            {
                int index = args.Parameters["Template"].IndexOf(',');
                Assert.IsTrue(index >= 0, "Invalid return value from dialog");
                string path = StringUtil.Left(args.Parameters["Template"], index);
                string name = StringUtil.Mid(args.Parameters["Template"], index + 1);
                Database database = GetDatabase(args.Parameters["database"]);
                Item parent = GetItem(database, args.Parameters["id"], args.Parameters["language"]);
                if (parent == null)
                {
                    SheerResponse.Alert("Parent item not found.");
                    args.AbortPipeline();
                    return;
                }

                if (!parent.Access.CanCreate())
                {
                    SheerResponse.Alert("You do not have permission to create items here.");
                    args.AbortPipeline();
                    return;
                }

                Item item = database.GetItem(path);
                if (item == null)
                {
                    SheerResponse.Alert("Item not found.");
                    args.AbortPipeline();
                    return;
                }
                
                History.Template = item.ID.ToString();
                Item added = null;
                if (item.TemplateID == TemplateIDs.Template)
                {
                    Log.Audit(this, "Add from template: {0}", new string[] { AuditFormatter.FormatItem(item) });
                    TemplateItem template = item;
                    added = Context.Workflow.AddItem(name, template, parent);
                }
                else
                {
                    Log.Audit(this, "Add from branch: {0}", new string[] { AuditFormatter.FormatItem(item) });
                    BranchItem branch = item;
                    added = Context.Workflow.AddItem(name, branch, parent);
                }

                if (added == null)
                {
                    SheerResponse.Alert("Something went terribly wrong when adding the item.");
                    args.AbortPipeline();
                    return;
                }

                args.Parameters["AddedId"] = added.ID.ToString();
            }
        }

        public void MoveAdded(ClientPipelineArgs args)
        {
            AssertArguments(args);
            Assert.ArgumentNotNullOrEmpty(args.Parameters["AddedId"], "AddedId");
            Item added = GetAddedItem(args);
            if (string.IsNullOrWhiteSpace(args.Parameters["InsertAfterId"]))
            {
                Items.MoveFirst(new [] { added });
            }
            
            SetSortorder(GetItemOrdering(added, args.Parameters["InsertAfterId"]));
        }

        private static void AssertArguments(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            Assert.ArgumentNotNullOrEmpty(args.Parameters["database"], "database");
            Assert.ArgumentNotNullOrEmpty(args.Parameters["language"], "language");
        }

        private static Item GetAddedItem(ClientPipelineArgs args)
        {
            AssertArguments(args);
            Assert.ArgumentNotNullOrEmpty(args.Parameters["AddedId"], "AddedId");
            return GetItem(GetDatabase(args.Parameters["database"]), args.Parameters["AddedId"], args.Parameters["language"]);
        }

        private static Item GetItem(Database database, string id, string language)
        {
            Assert.ArgumentNotNull(database, "database");
            Assert.ArgumentNotNullOrEmpty(id, "id");
            Assert.ArgumentNotNullOrEmpty(language, "language");
            return database.Items[id, Language.Parse(language)];
        }

        private static Database GetDatabase(string databaseName)
        {
            Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            return Factory.GetDatabase(databaseName);
        }

        private static IList<Item> GetItemOrdering(Item added, string insertAfterId)
        {
            IList<Item> ordering = new List<Item>();
            foreach (Item child in added.Parent.GetChildren())
            {
                ordering.Add(child);
                bool shouldAddAfter = string.Equals(child.ID.ToString(), insertAfterId);
                if (shouldAddAfter)
                {
                    ordering.Add(added);
                }
            }

            return ordering;
        }

        private static void SetSortorder(IList<Item> items)
        {
            Assert.ArgumentNotNull(items, "items");
            for (int i = 0; i < items.Count; i++)
            {
                int sortorder = (i + 1) * 100;
                SetSortorder(items[i], sortorder);
            }
        }

        private static void SetSortorder(Item item, int sortorder)
        {
            Assert.ArgumentNotNull(item, "item");
            if (item.Access.CanWrite() && !item.Appearance.ReadOnly)
            {
                item.Editing.BeginEdit();
                item[FieldIDs.Sortorder] = sortorder.ToString();
                item.Editing.EndEdit();
            }
        }
    }
}

In the StoreTemplateResult method, we store the ID of the template selected in the ‘Insert from Template’ dialog. This dialog is launched by clicking the ‘Insert from Template’ menu option in the item context menu — an example of this can be seen in my test run near the bottom of this post.

The EnsureParentAndChildren method makes certain the parent item exists — we want to be sure another user did not delete it in another Sitecore session — and ascertains if the parent item has children.

Logic in the GetInsertAfterId method launches another dialog when the parent item does have children. This dialog prompts the user to select a sibling item to precede the new item. If the ‘Cancel’ button is clicked, the item will be inserted before all sibling items.

The AddItemFromTemplate method basically contains the same logic that can be found in the Execute method in the Sitecore.Shell.Framework.Pipelines.AddFromTemplate class in Sitecore.Kernel.dll, albeit with a few minor changes — I removed some of the nested if/else conditionals, and stored the ID of the newly created item, which is needed when reordering the sibling items (this is how we move the new item after the selected sibling item).

The MoveAdded method is where we reorder the siblings items with the newly created item so that the new item follows the selected sibling. If there is no selected sibling, we just move the new item to the first position.

I then put all of the above together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <uiAddFromTemplate>
        <processor mode="on" patch:after="processor[@type='Sitecore.Shell.Framework.Pipelines.AddFromTemplate,Sitecore.Kernel' and @method='GetTemplate']"
                   type="Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox" method="StoreTemplateResult"/>
        <processor mode="on" patch:after="processor[@type='Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox' and @method='StoreTemplateResult']"
                   type="Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox" method="EnsureParentAndChildren"/>
        <processor mode="on" patch:after="processor[@type='Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox' and @method='EnsureParentAndChildren']"
                   type="Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox" method="GetInsertAfterId">
          <SelectButtonText>Insert After</SelectButtonText>
          <ModalIcon>Applications/32x32/nav_up_right_blue.png</ModalIcon>
          <ModalTitle>Select Item to Insert After</ModalTitle>
          <ModalInstructions>Select the item you would like to insert after. If you would like to insert before the first item, just click 'Cancel'.</ModalInstructions>
        </processor>
        <processor mode="on" patch:instead="processor[@type='Sitecore.Shell.Framework.Pipelines.AddFromTemplate,Sitecore.Kernel' and @method='Execute']"
                   type="Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox" method="AddItemFromTemplate" />
        <processor mode="on" patch:after="processor[@type='Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox' and @method='AddItemFromTemplate']"
                   type="Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox" method="MoveAdded" />
      </uiAddFromTemplate> 
    </processors>
  </sitecore>
</configuration>

Let’s test this out.

This is how my content tree looked before adding new items:

no-new-items

I right-clicked on my Home item to launch its context menu, and clicked ‘Insert from Template’:

item-context-menu-insert-from-template

I was presented with the “out of the box” ‘Insert from Template’ dialog, and selected a template:

insert-from-template-dialog

Next I was prompted to select a sibling item to insert the new item after:

selected-insert-after

As you can see the new item now resides after the selected sibling:

was-inserted-after

If you have any thoughts on this, or other ideas around modifying the uiAddFromTemplate pipeline, please share in a comment below.

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:

lighthouse-small

As you can see its width is much larger than 1024 pixels:

lighthouse-properties

After uploading the lighthouse image into the media library, its width was set to the maximum specified, and its height was scaled down proportionally:

resized-during-upload

If you have any thoughts on this, or other ideas on resizing images uploaded to the media library, please drop a comment.