Quantcast
Channel: Pipeline – SitecoreJunkie.com
Viewing all 51 articles
Browse latest View live

Where Is This Field Defined? Add ‘Goto Template’ Links for Fields in the Sitecore Content Editor

$
0
0

About a month ago, I read this answer on Stack Overflow by Sitecore MVP Dan Solovay, and thought to myself “what could I do with a custom EditorFormatter that might be useful?”

Today, I came up with an idea that might be useful, especially when working with many levels of nested base templates: having a ‘Goto Template’ link — or button depending on your naming preference, although I will refer to these as links throughout this post since they are hyperlinks — for each field that, when clicked, will bring you to the Sitecore template where the field is defined.

I first defined a class to manage the display state of our ‘Goto Template’ links in the Content Editor:

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

using Sitecore.Web.UI.HtmlControls;

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public class GotoTemplateLinksSettings
    {
        private const string RegistrySettingKey = "/Current_User/Content Editor/Goto Template Links";
        private const string RegistrySettingOnValue = "on";

        private static volatile GotoTemplateLinksSettings current;
        private static object lockObject = new Object();

        public static GotoTemplateLinksSettings Current
        {
            get
            {
                if (current == null)
                {
                    lock (lockObject)
                    {
                        if (current == null)
                            current = new GotoTemplateLinksSettings();
                    }
                }

                return current;
            }
        }

        private GotoTemplateLinksSettings()
        {
        }

        public bool IsOn()
        {
            return Registry.GetString(RegistrySettingKey) == RegistrySettingOnValue;
        }

        public void TurnOn()
        {
            Registry.SetString(RegistrySettingKey, RegistrySettingOnValue);
        }

        public void TurnOff()
        {
            Registry.SetString(RegistrySettingKey, string.Empty);
        }
    }
}

I decided to make the above class be a Singleton — there should only be one central place where the display state of our links is toggled.

I created a subclass of Sitecore.Shell.Applications.ContentEditor.EditorFormatter, and overrode the RenderField(Control, Editor.Field, bool) method to embed additional logic to render a ‘Goto Template’ link for each field in the Content Editor:

using System.Web.UI;

using Sitecore;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Shell.Applications.ContentManager;

using Sitecore.Sandbox.Utilities.ClientSettings;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class GotoTemplateEditorFormatter : EditorFormatter
    {
        public override void RenderField(Control parent, Editor.Field field, bool readOnly)
        {
            Assert.ArgumentNotNull(parent, "parent");
            Assert.ArgumentNotNull(field, "field");
            Field itemField = field.ItemField;
            Item fieldType = GetFieldType(itemField);

            if (fieldType != null)
            {
                if (!itemField.CanWrite)
                {
                    readOnly = true;
                }

                RenderMarkerBegin(parent, field.ControlID);
                RenderMenuButtons(parent, field, fieldType, readOnly);
                RenderLabel(parent, field, fieldType, readOnly);
                AddGotoTemplateLinkIfCanView(parent, field);
                RenderField(parent, field, fieldType, readOnly);
                RenderMarkerEnd(parent);
            }
        }

        public void AddGotoTemplateLinkIfCanView(Control parent, Editor.Field field)
        {
            if (CanViewGotoTemplateLink())
            {
                AddGotoTemplateLink(parent, field);
            }
        }

        private static bool CanViewGotoTemplateLink()
        {
            return IsGotoTemplateLinksOn();
        }

        private static bool IsGotoTemplateLinksOn()
        {
            return GotoTemplateLinksSettings.Current.IsOn();
        }

        public void AddGotoTemplateLink(Control parent, Editor.Field field)
        {
            Assert.ArgumentNotNull(parent, "parent");
            Assert.ArgumentNotNull(field, "field");
            AddLiteralControl(parent, CreateGotoTemplateLink(field));
        }

        private static string CreateGotoTemplateLink(Editor.Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            return string.Format("<a title=\"Navigate to the template where this field is defined.\" style=\"float: right;position:absolute;margin-top:-20px;right:15px;\" href=\"#\" onclick=\"{0}\">{1}</a>", CreateGotoTemplateJavascript(field), CreateGotoTemplateLinkText());
        }

        private static string CreateGotoTemplateJavascript(Editor.Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            return string.Format("javascript:scForm.postRequest('', '', '','item:load(id={0})');return false;", field.TemplateField.Template.ID);
        }

        private static string CreateGotoTemplateLinkText()
        {
            return "<img style=\"border: 0;\" src=\"/~/icon/Applications/16x16/information2.png\" width=\"16\" height=\"16\" />";
        }
    }
}

‘Goto Template’ links are only rendered to the Sitecore Client when the display state for showing them is turned on.

Plus, each ‘Goto Template’ link is locked and loaded to invoke the item load command to navigate to the template item where the field is defined.

As highlighted by Dan in his Stack Overflow answer above, I created a new Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor pipeline processor, and hooked in an instance of the GotoTemplateEditorFormatter class defined above:

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

using Sitecore.Diagnostics;

using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor
{
    public class RenderStandardContentEditor
    {
        public void Process(RenderContentEditorArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.EditorFormatter = CreateNewGotoTemplateEditorFormatter(args);
            args.EditorFormatter.RenderSections(args.Parent, args.Sections, args.ReadOnly);
        }

        private static EditorFormatter CreateNewGotoTemplateEditorFormatter(RenderContentEditorArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.EditorFormatter, "args.EditorFormatter");
            return new GotoTemplateEditorFormatter 
            { 
                Arguments = args.EditorFormatter.Arguments, 
                IsFieldEditor = args.EditorFormatter.IsFieldEditor 
            };
        }
    }
}

Now we need a way to toggle the display state of our ‘Goto Template’ links. I decided to create a command to turn this state on and off:

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

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web.UI.HtmlControls;

using Sitecore.Sandbox.Utilities.ClientSettings;

namespace Sitecore.Sandbox.Commands
{
    public class ToggleGotoTemplateLinks : Command
    {
        public override void Execute(CommandContext commandContext)
        {
            ToggleGotoTemplateLinksOn();
            Refresh(commandContext);
        }

        private static void ToggleGotoTemplateLinksOn()
        {
            GotoTemplateLinksSettings gotoTemplateLinksSettings = GotoTemplateLinksSettings.Current;

            if (!gotoTemplateLinksSettings.IsOn())
            {
                gotoTemplateLinksSettings.TurnOn();
            }
            else
            {
                gotoTemplateLinksSettings.TurnOff();
            }
        }

        private static void Refresh(CommandContext commandContext)
        {
            Refresh(GetItem(commandContext));
        }

        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 commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            return commandContext.Items.FirstOrDefault();
        }

        public override CommandState QueryState(CommandContext context)
        {
            if (!GotoTemplateLinksSettings.Current.IsOn())
            {
                return CommandState.Enabled;
            }

            return CommandState.Down;
        }
    }
}

I registered the pipeline processor defined above coupled with the toggle command in a patch include configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="contenteditor:togglegototemplatelinks" type="Sitecore.Sandbox.Commands.ToggleGotoTemplateLinks, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <renderContentEditor>
        <processor 
            patch:instead="*[@type='Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderStandardContentEditor, Sitecore.Client']" 
            type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderStandardContentEditor, Sitecore.Sandbox"/>
      </renderContentEditor>
      
    </pipelines>
  </sitecore>
</configuration>

I then added a toggle checkbox to the View ribbon, and wired up the ToggleGotoTemplateLinks command to it:

goto-template-links-view-checkbox

When the ‘Goto Template Links’ checkbox is checked, ‘Goto Template’ links are displayed for each field in the Content Editor:

goto-template-links-turned-on

When unchecked, the ‘Goto Template’ links are not rendered:

goto-template-links-turned-off

Let’s try it out.

Let’s click one of these ‘Goto Template’ links and see what it does, or where it takes us:

Home-Data-Title-Goto-Template-Link

It brought us to the template where the Title field is defined:

Goto-Template-Title-Template

Let’s try another. How about the ‘Created By’ field?

home-created-by-goto-template-link

Its link brought us to its template:

Goto-Template-CreatedBy-Template

Without a doubt, the functionality above would be useful to developers and advanced users.

I’ve been trying to figure out other potential uses for other subclasses of EditorFormatter. Can you think of other ways we could leverage a custom EditorFormatter, especially one for non-technical Sitecore users? If you have any ideas, please drop a comment. :)



Reuse Sitecore Data Template Fields by Pulling Them Up Into a Base Template

$
0
0

How many times have you discovered that you need to reuse some fields defined in a data template, but don’t want to use that data template as a base template since it defines a bunch of fields that you don’t want to use?

What do you usually do in such a situation?

In the past, I would have created a new data template, moved the fields I would like to reuse underneath it, followed by setting that new template as a base template on the data template I moved the fields from.

I’ve spoken with other Sitecore developers on what they do in this situation, and one developer conveyed he duplicates the template, and delete fields that are not to be reused — thus creating a new base template. Though this might save time, I would advise against it — any code referencing fields by IDs will break using this paradigm for creating a new base template.

Martin Fowler — in his book
Refactoring: Improving the Design of Existing Code — defined the refactoring technique ‘Pull Up Field’ for moving fields from a class up into its base class — dubbed superclass for the Java inclined audience.

One should employ this refactoring technique when other subclasses of the base class are to reuse the fields being pulled up.

When I first encountered this refactoring technique in Fowler’s book, I pondered over how useful this would be in the Sitecore data template world, and decided to build two tools for pulling up fields into a base template. One tool will create a new base template, and the other will allow users to move fields to an existing base template.

The following class defines methods that will be used by two new client pipelines I will define below:

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

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

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class PullUpFieldsIntoBaseTemplate
    {
        private const char IDDelimiter = '|';
        private const string BaseTemplateFieldName = "__Base template";
        
        public void EnsureTemplate(ClientPipelineArgs args)
        {
            Item item = GetItem(args);
            if (!IsTemplate(item))
            {
                CancelOperation(args, "The supplied item is not a template!");
            }
        }

        private static bool IsTemplate(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return TemplateManager.IsTemplate(item);
        }

        public void SelectFields(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!args.IsPostBack)
            {
                UrlString urlString = new UrlString(UIUtil.GetUri("control:TreeListExEditor"));
                UrlHandle urlHandle = new UrlHandle();
                urlHandle["title"] = "Select Fields";
                urlHandle["text"] = "Select the fields to pull up into a base template.";
                urlHandle["source"] = GetSelectFieldsDialogSource(args);
                urlHandle.Add(urlString);
                SheerResponse.ShowModalDialog(urlString.ToString(), "800px", "500px", string.Empty, true);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["fieldIds"] = args.Result;
                args.IsPostBack = false;
            }
            else
            {
                CancelOperation(args);
            }
        }

        private static string GetSelectFieldsDialogSource(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(args.Parameters["database"], "args.Parameters[\"database\"]");
            Assert.ArgumentNotNullOrEmpty(args.Parameters["templateId"], "args.Parameters[\"templateId\"]");
            return string.Format
            (
                "AllowMultipleSelection=true&DatabaseName={0}&DataSource={1}&IncludeTemplatesForSelection=Template field",
                args.Parameters["database"],
                args.Parameters["templateId"]
            );
        }

        public void ChooseBaseTemplateName(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                SheerResponse.Input("Enter the name of the base template", string.Concat("New Base Template 1"));
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["baseTemplateName"] = args.Result;
                args.IsPostBack = false;
            }
            else
            {
                CancelOperation(args);
            }
        }

        public void ChooseBaseTemplateLocation(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                Dialogs.BrowseItem
                (
                    "Select Base Template Location",
                    "Select the parent item under which the new base template will be created.",
                    args.Parameters["icon"],
                    "OK",
                    "/sitecore/templates",
                    args.Parameters["templateId"]
                );

                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["baseTemplateParentId"] = args.Result;
                args.IsPostBack = false;
            }
            else
            {
                CancelOperation(args);
            }
        }

        public void CreateNewBaseTemplate(ClientPipelineArgs args)
        {
            Database database = GetDatabase(args.Parameters["database"]);
            Item baseTemplateParent = database.GetItem(args.Parameters["baseTemplateParentId"]);
            Item baseTemplate = baseTemplateParent.Add(args.Parameters["baseTemplateName"], new TemplateID(TemplateIDs.Template));
            SetBaseTemplateField(baseTemplate, TemplateIDs.StandardTemplate);
            args.Parameters["baseTemplateId"] = baseTemplate.ID.ToString();
        }

        public void ChooseExistingBaseTemplate(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                ItemListerOptions itemListerOptions = new ItemListerOptions
                {
                    ButtonText = "OK",
                    Icon = args.Parameters["icon"],
                    Title = "Select Base Template",
                    Text = "Select the base template for pulling up the fields."
                };

                TemplateItem templateItem = GetItem(args);
                itemListerOptions.Items = ExcludeStandardTemplate(templateItem.BaseTemplates).Select(baseTemplate => baseTemplate.InnerItem).ToList();
                itemListerOptions.AddTemplate(TemplateIDs.Template);
                SheerResponse.ShowModalDialog(itemListerOptions.ToUrlString().ToString(), true);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["baseTemplateId"] = args.Result;
                args.IsPostBack = false;
            }
            else
            {
                CancelOperation(args);
            }
        }

        private static IEnumerable<TemplateItem> ExcludeStandardTemplate(IEnumerable<TemplateItem> baseTemplates)
        {
            if (baseTemplates != null && baseTemplates.Any())
            {
                return baseTemplates.Where(baseTemplate => baseTemplate.ID != TemplateIDs.StandardTemplate);
            }

            return baseTemplates;
        }

        public void EnsureCanCreateAtBaseTemplateLocation(ClientPipelineArgs args)
        {
            Item baseTemplateParent = GetItem(args.Parameters["database"], args.Parameters["baseTemplateParentId"]);
            if (!baseTemplateParent.Access.CanCreate())
            {
                CancelOperation(args, "You cannot create an item at the selected location!");
            }
        }

        public void Execute(ClientPipelineArgs args)
        {
            Database database = GetDatabase(args.Parameters["database"]);
            Item baseTemplate = database.GetItem(args.Parameters["baseTemplateId"]);
            SetBaseTemplateField(baseTemplate, TemplateIDs.StandardTemplate);
            IDictionary<SectionInformation, IList<Item>> sectionsWithFields = GetSectionsWithFields(database, args.Parameters["fieldIds"]);

            foreach (SectionInformation sectionInformation in sectionsWithFields.Keys)
            {
                IEnumerable<Item> fields = sectionsWithFields[sectionInformation];
                MoveFieldsToAppropriateSection(baseTemplate, sectionInformation, fields);
            }

            Item templateItem = database.GetItem(args.Parameters["templateId"]);
            ListString baseTemplateIDs = new ListString(templateItem[BaseTemplateFieldName], IDDelimiter);
            baseTemplateIDs.Add(baseTemplate.ID.ToString());
            SetBaseTemplateField(templateItem, baseTemplateIDs);
            Refresh(templateItem);
        }

        private static void MoveFieldsToAppropriateSection(Item baseTemplate, SectionInformation sectionInformation, IEnumerable<Item> fields)
        {
            TemplateItem templateItem = baseTemplate;
            TemplateSectionItem templateSectionItem = templateItem.GetSection(sectionInformation.Section.Name);

            if (templateSectionItem != null)
            {
                MoveFields(templateSectionItem, fields);
            }
            else
            {
                Item sectionItemCopy = sectionInformation.Section.CopyTo(baseTemplate, sectionInformation.Section.Name, ID.NewID, false);
                MoveFields(sectionItemCopy, fields);
            }

            if (!sectionInformation.Section.GetChildren().Any())
            {
                sectionInformation.Section.Delete();
            }
        }

        private static void MoveFields(TemplateSectionItem templateSectionItem, IEnumerable<Item> fields)
        {
            Assert.ArgumentNotNull(templateSectionItem, "templateSectionItem");
            Assert.ArgumentNotNull(fields, "fields");
            foreach (Item field in fields)
            {
                field.MoveTo(templateSectionItem.InnerItem);
            }
        }

        private static void SetBaseTemplateField(Item templateItem, object baseTemplateIds)
        {
            Assert.ArgumentNotNull(templateItem, "templateItem");
            templateItem.Editing.BeginEdit();
            templateItem[BaseTemplateFieldName] = EnsureDistinct(baseTemplateIds.ToString(), IDDelimiter);
            templateItem.Editing.EndEdit();
        }

        private static string EnsureDistinct(string strings, char delimiter)
        {
            return string.Join(delimiter.ToString(), EnsureDistinct(strings.ToString().Split(new[] { delimiter })));
        }

        private static IEnumerable<string> EnsureDistinct(IEnumerable<string> strings)
        {
            return strings.Distinct();
        }

        private static IDictionary<SectionInformation, IList<Item>> GetSectionsWithFields(Database database, string fieldIds)
        {
            IDictionary<SectionInformation, IList<Item>> sectionsWithFields = new Dictionary<SectionInformation, IList<Item>>();

            foreach (TemplateFieldItem field in GetFields(database, fieldIds))
            {
                SectionInformation sectionInformation = new SectionInformation(field.Section.InnerItem);
                if (sectionsWithFields.ContainsKey(sectionInformation))
                {
                    sectionsWithFields[sectionInformation].Add(field.InnerItem);
                }
                else
                {
                    sectionsWithFields.Add(sectionInformation, new List<Item>(new[] { field.InnerItem }));
                }
            }

            return sectionsWithFields;
        }

        private static IEnumerable<TemplateFieldItem> GetFields(Database database, string fieldIds)
        {
            ListString fieldIdsListString = new ListString(fieldIds, IDDelimiter);
            IList<TemplateFieldItem> fields = new List<TemplateFieldItem>();

            foreach (string fieldId in fieldIdsListString)
            {
                fields.Add(database.GetItem(fieldId));
            }

            return fields;
        }

        private static Item GetItem(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Item item = GetItem(args.Parameters["database"], args.Parameters["templateId"]);
            Assert.IsNotNull(item, "Item cannot be null!");
            return item;
        }

        private static Item GetItem(string databaseName, string itemId)
        {
            Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            Assert.ArgumentNotNullOrEmpty(itemId, "itemId");
            ID id = GetID(itemId);
            Assert.IsTrue(!ID.IsNullOrEmpty(id), "itemId is not valid!");
            Database database = GetDatabase(databaseName);
            Assert.IsNotNull(database, "Database is not valid!");
            return database.GetItem(id);
        }

        private static ID GetID(string itemId)
        {
            ID id;
            if (ID.TryParse(itemId, out id))
            {
                return id;
            }

            return ID.Null;
        }

        private static Database GetDatabase(string databaseName)
        {
            Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            return Factory.GetDatabase(databaseName);
        }
        
        private static void CancelOperation(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            CancelOperation(args, "Operation has been cancelled!");
        }

        private static void CancelOperation(ClientPipelineArgs args, string alertMessage)
        {
            Assert.ArgumentNotNull(args, "args");

            if (!string.IsNullOrEmpty(alertMessage))
            {
                SheerResponse.Alert(alertMessage);
            }

            args.AbortPipeline();
        }

        private static void Refresh(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", item.ID), 1);
        }

        public class SectionInformation : IEquatable<SectionInformation>
        {
            public Item Section { get; set; }

            public SectionInformation(Item section)
            {
                Assert.ArgumentNotNull(section, "section");
                Section = section;
            }

            public override int GetHashCode()
            {
                return Section.ID.Guid.GetHashCode();
            }

            public override bool Equals(object other)
            {
                return Equals(other as SectionInformation);
            }

            public bool Equals(SectionInformation other)
            {
                return other != null 
                        && Section.ID.Guid == Section.ID.Guid;
            }
        }
    }
}

This above class defines methods that will be used as pipeline processor steps to prompt users for input — via dialogs — needed for pulling up fields to a new or existing base template.

I then defined commands to launch the pipelines I will define later on. I employed the Template method pattern here — each subclass must define its icon path, and the client pipeline it depends on after being clicked:

using System.Collections.Specialized;
using System.Linq;

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

namespace Sitecore.Sandbox.Commands.Base
{
    public abstract class PullUpFieldsCommand : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Context.ClientPage.Start(GetPullUpFieldsClientPipelineName(), CreateNewParameters(GetItem(context)));
        }

        protected abstract string GetPullUpFieldsClientPipelineName();

        private NameValueCollection CreateNewParameters(Item templateItem)
        {
            return new NameValueCollection 
            {
                {"icon", GetIcon()},
                {"database", templateItem.Database.Name},
                {"templateId", templateItem.ID.ToString()}
            };
        }

        protected abstract string GetIcon();

        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            if (ShouldEnable(GetItem(context)))
            {
                return CommandState.Enabled;
            }

            return CommandState.Hidden;
        }

        protected static Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Items, "context.Items");
            Assert.ArgumentCondition(context.Items.Any(), "context.Items", "At least one item must be present!");
            return context.Items.FirstOrDefault();
        }

        private static bool ShouldEnable(Item item)
        {
            return item != null && IsTemplate(item);
        }

        private static bool IsTemplate(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return TemplateManager.IsTemplate(item);
        }
    }
}

The command for pulling up fields into a new base template:

using Sitecore.Sandbox.Commands.Base;

namespace Sitecore.Sandbox.Commands
{
    public class PullUpFieldsIntoNewBaseTemplate : PullUpFieldsCommand
    {
        protected override string GetIcon()
        {
            return "Business/32x32/up_plus.png";
        }

        protected override string GetPullUpFieldsClientPipelineName()
        {
            return "uiPullUpFieldsIntoNewBaseTemplate";
        }
    }
}

The command for pulling up fields into an existing base template — it will be visible only when the template has base templates, not including the Standard Template:

using System.Linq;

using Sitecore.Sandbox.Commands.Base;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Commands
{
    public class PullUpFieldsIntoExistingBaseTemplate : PullUpFieldsCommand
    {
        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            bool shouldEnabled = base.QueryState(context) == CommandState.Enabled 
                                    && DoesTemplateHaveBaseTemplates(context);
            if (shouldEnabled)
            {
                return CommandState.Enabled;
            }

            return CommandState.Hidden;
        }

        private static bool DoesTemplateHaveBaseTemplates(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            TemplateItem templateItem = GetItem(context);
            return templateItem.BaseTemplates.Count() > 1 
                    || (templateItem.BaseTemplates.Any() 
                            && templateItem.BaseTemplates.FirstOrDefault().ID != TemplateIDs.StandardTemplate);
        }

        protected override string GetIcon()
        {
            return "Business/32x32/up_minus.png";
        }

        protected override string GetPullUpFieldsClientPipelineName()
        {
            return "uiPullUpFieldsIntoExistingBaseTemplate";
        }
    }
}

I’ve omitted how I mapped these to context menu buttons in the core database. If you are unfamiliar with how this is done, please take a look at my first and second posts on augmenting the item context menu to see how this is done.

I registered my commands, and glued methods in the PullUpFieldsIntoBaseTemplate class together into two client pipelines in a configuration include file:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:pullupfieldsintonewbasetemplate" type="Sitecore.Sandbox.Commands.PullUpFieldsIntoNewBaseTemplate,Sitecore.Sandbox"/>
      <command name="item:pullupfieldsintoexistingbasetemplate" type="Sitecore.Sandbox.Commands.PullUpFieldsIntoExistingBaseTemplate,Sitecore.Sandbox"/>
    </commands>
    <processors>
      <uiPullUpFieldsIntoNewBaseTemplate>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="EnsureTemplate"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="SelectFields"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="ChooseBaseTemplateName"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="ChooseBaseTemplateLocation"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="EnsureCanCreateAtBaseTemplateLocation"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="CreateNewBaseTemplate"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="Execute"/>
      </uiPullUpFieldsIntoNewBaseTemplate>
      <uiPullUpFieldsIntoExistingBaseTemplate>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="EnsureTemplate"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="SelectFields"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="ChooseExistingBaseTemplate"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="Execute"/>
      </uiPullUpFieldsIntoExistingBaseTemplate>
    </processors>
  </sitecore>
</configuration>

The uiPullUpFieldsIntoNewBaseTemplate client pipeline contains processor steps to present the user with dialogs for pulling up fields into a new base template that is created during one of those steps.

The uiPullUpFieldsIntoExistingBaseTemplate pipeline prompts users to select a base template from the list of base templates set on the template.

Let’s see this in action.

I created a template with some fields for testing:

pull-up-fields-landing-page

I right-clicked the on my Landing Page template to launch its context menu, and clicked on the ‘Pull Up Fields Into New Base Template’ context menu option:

pull-up-new-base-template-context-menu

The following dialog was presented to me for selecting fields to pull up:

I clicked the ‘OK’ button, and was prompted to input a name for a new base template. I entered ‘Page Base’:

pull-up-page-base

After clicking ‘OK’, I was asked to select the location where I want to store my new base template:

pull-up-base-template-folder

Not too long after clicking ‘OK’ again, I see that my new base template was created, and the Title field was moved into it:

pull-up-page-base-created

Further, I see that my Landing Page template now has Page Base as one of its base templates:

pull-up-page-base-set-base-template

I right-clicked again, and was presented with an additional context menu option to move fields into an existing base template — I clicked on this option:

pull-up-existing-base-template-context-menu

I then selected the remaining field under the Page Data section:

pull-up-selected-text-field

After clicking ‘OK’, a dialog presented me with a list of base templates to choose from — in this case there was only one base template to choose:

pull-up-selected-page-base-template

After clicking ‘OK’, I see that the Text field was moved into the Page Base template, and the Page Data section under my Landing Page template was removed:

pull-up-text-field

I hope the above tools will save you time in pulling up fields into base templates.

If you can think of other development tools that would be useful in the Sitecore client, please drop a comment.

Addendum

I have put a polished version of the above onto the Sitecore Marketplace.


Swap Out Sitecore Layouts and Sublayouts Dynamically Based on a Theme

$
0
0

Out of the box, cloned items in Sitecore will retain the presentation components of their source items, and teasing these out could potentially lead to a mess.

I recently worked on a project where I had to architect a solution to avoid such a mess — this solution would ensure that items cloned across different websites in the same Sitecore instance could have a different look and feel from their source items.

Though I can’t show you what I did specifically on that project, I did go about creating a different solution just for you — yes, you — and this post showcases the fruit of that endeavor.

This is the basic idea of the solution:

For every sublayout (or the layout) set on an item, try to find a variant of that sublayout (or the layout) in a subfolder with the name of the selected theme. Otherwise, use the original.

For example, if /layouts/sublayouts/MySublayout.ascx is mapped to a sublayout set on the item and our selected theme is named “My Cool Theme”, try to find and use /layouts/sublayouts/My Cool Theme/MySublayout.ascx on the file system. If it does not exist, just use /layouts/sublayouts/MySublayout.ascx.

To start, I created some “theme” items. These items represent folders on my file system, and these folders house a specific layout and sublayouts pertinent to the theme. I set the parent folder of these items to be the source of the Theme droplist on my home page item:

theme-droplist

In this solution, I defined objects as detectors — objects that ascertain particular conditions, depending on encapsulated logic within the detector class for the given source. This is the contract for all detectors in my solution:

namespace Sitecore.Sandbox.Utilities.Detectors.Base
{
    public interface IDetector<T>
    {
        T Source { get; set; }

        bool Detect();
    }
}

In this solution, I am leveraging some string detectors:

namespace Sitecore.Sandbox.Utilities.Detectors.Base
{
    public interface IStringDetector : IDetector<string>
    {
    }
}

The first string detector I created was one to ascertain whether a file exists on the file system:

using System.IO;

using Sitecore.Diagnostics;
using Sitecore.Sandbox.Utilities.Detectors.Base;

namespace Sitecore.Sandbox.Utilities.Detectors
{
    public class FileExistsDetector : IStringDetector
    {
        private string _Source;
        public string Source
        {
            get
            {
                return _Source;
            }
            set
            {
                Assert.ArgumentNotNull(value, "Source");
                _Source = value;
            }
        }

        private FileExistsDetector()
        {
        }

        private FileExistsDetector(string source)
        {
            SetSource(source);
        }

        private void SetSource(string source)
        {
            Source = source;
        }

        public bool Detect()
        {
            Assert.ArgumentNotNull(Source, "Source");
            return File.Exists(Source);
        }

        public static IStringDetector CreateNewFileExistsDetector()
        {
            return new FileExistsDetector();
        }

        public static IStringDetector CreateNewFileExistsDetector(string source)
        {
            return new FileExistsDetector(source);
        }
    }
}

The detector above is not web server aware — paths containing forward slashes will not be found. This led to the creation of the following decorator to convert server to file system paths:

using System.IO;
using System.Web;

using Sitecore.Diagnostics;
using Sitecore.Sandbox.Utilities.Detectors.Base;

namespace Sitecore.Sandbox.Utilities.Detectors
{
    public class ServerFileExistsDetector : IStringDetector
    {
        private HttpServerUtility HttpServerUtility { get; set; }
        private IStringDetector Detector { get; set; }
        
        public string Source
        {
            get
            {
                return Detector.Source;
            }
            set
            {
                Assert.ArgumentNotNull(value, "Source");
                Detector.Source = HttpServerUtility.MapPath(value);
            }
        }

        private ServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector)
        {
            SetHttpServerUtility(httpServerUtility);
            SetDetector(detector);
        }

        private ServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source)
        {
            SetHttpServerUtility(httpServerUtility);
            SetDetector(detector);
            SetSource(source);
        }

        private void SetHttpServerUtility(HttpServerUtility httpServerUtility)
        {
            Assert.ArgumentNotNull(httpServerUtility, "httpServerUtility");
            HttpServerUtility = httpServerUtility;
        }

        private void SetDetector(IStringDetector detector)
        {
            Assert.ArgumentNotNull(detector, "detector");
            Detector = detector;
        }

        private void SetSource(string source)
        {
            Source = source;
        }

        public bool Detect()
        {
            Assert.ArgumentNotNull(Source, "Source");
            return Detector.Detect();
        }

        public static IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector)
        {
            return new ServerFileExistsDetector(httpServerUtility, detector);
        }

        public static IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source)
        {
            return new ServerFileExistsDetector(httpServerUtility, detector, source);
        }
    }
}

I thought it would be best to control the instantiation of the string detectors above into a factory class:

using System.Web;

namespace Sitecore.Sandbox.Utilities.Detectors.Base
{
    public interface IStringDetectorFactory
    {
        IStringDetector CreateNewFileExistsDetector();

        IStringDetector CreateNewFileExistsDetector(string source);

        IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility);

        IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector);

        IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source);
    }
}
using System.Web;

using Sitecore.Sandbox.Utilities.Detectors.Base;

namespace Sitecore.Sandbox.Utilities.Detectors
{
    public class StringDetectorFactory : IStringDetectorFactory
    {
        private StringDetectorFactory()
        {
        }

        public IStringDetector CreateNewFileExistsDetector()
        {
            return FileExistsDetector.CreateNewFileExistsDetector();
        }

        public IStringDetector CreateNewFileExistsDetector(string source)
        {
            return FileExistsDetector.CreateNewFileExistsDetector(source);
        }

        public IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility)
        {
            return ServerFileExistsDetector.CreateNewServerFileExistsDetector(httpServerUtility, CreateNewFileExistsDetector());
        }

        public IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector)
        {
            return ServerFileExistsDetector.CreateNewServerFileExistsDetector(httpServerUtility, detector);
        }

        public IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source)
        {
            return ServerFileExistsDetector.CreateNewServerFileExistsDetector(httpServerUtility, detector, source);
        }

        public static IStringDetectorFactory CreateNewStringDetectorFactory()
        {
            return new StringDetectorFactory();
        }
    }
}

I then needed a way to ascertain if an item is a sublayout — we shouldn’t be messing with presentation components that are not sublayouts (not including layouts which is handled in a different place):

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Utilities.Detectors.Base
{
    public interface IItemDetector : IDetector<Item>
    {
    }
}
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.Detectors.Base;

namespace Sitecore.Sandbox.Utilities.Detectors
{
    public class SublayoutDetector : IItemDetector
    {
        private Item _Source;
        public Item Source
        {
            get
            {
                return _Source;
            }
            set
            {
                Assert.ArgumentNotNull(value, "Source");
                _Source = value;
            }
        }

        private SublayoutDetector()
        {
        }

        private SublayoutDetector(Item source)
        {
            SetSource(source);
        }

        private void SetSource(Item source)
        {
            Source = source;
        }

        public bool Detect()
        {
            Assert.ArgumentNotNull(Source, "Source");
            return Source.TemplateID == TemplateIDs.Sublayout;
        }

        public static IItemDetector CreateNewSublayoutDetector()
        {
            return new SublayoutDetector();
        }

        public static IItemDetector CreateNewSublayoutDetector(Item source)
        {
            return new SublayoutDetector(source);
        }
    }
}

I then reused the concept of manipulators — objects that transform an instance of an object into a different instance — from a prior blog post. I forget the post I used manipulators in, so I have decided to re-post the base contract for all manipulator classes:

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IManipulator<T>
    {
        T Manipulate(T source);
    }
}

Yes, we will be manipulating strings. :)

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IStringManipulator : IManipulator<string>
    {
    }
}

I also defined another interface containing an accessor and mutator for a string representing the selected theme:

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IThemeFolder
    {
        string ThemeFolder { get; set; }
    }
}

I created a class that will wedge in a theme folder name into a file path — it is inserted after the last forward slash in the file path — and returns the resulting string:

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IThemeFilePathManipulator : IStringManipulator, IThemeFolder
    {
    }
}
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.Manipulators.Base;

namespace Sitecore.Sandbox.Utilities.Manipulators
{
    public class ThemeFilePathManipulator : IThemeFilePathManipulator
    {
        private const string Slash = "/";
        private const int NotFoundIndex = -1;

        private string _ThemeFolder;
        public string ThemeFolder 
        {
            get
            {
                return _ThemeFolder;
            }
            set
            {
                _ThemeFolder = value;
            }
        }

        private ThemeFilePathManipulator()
        {
        }

        private ThemeFilePathManipulator(string themeFolder)
        {
            SetThemeFolder(themeFolder);
        }

        public string Manipulate(string source)
        {
            Assert.ArgumentNotNullOrEmpty(source, "source");
            int lastIndex = source.LastIndexOf(Slash);
            bool canManipulate = !string.IsNullOrEmpty(ThemeFolder) && lastIndex > NotFoundIndex;
            if (canManipulate)
            {
                return source.Insert(lastIndex + 1, string.Concat(ThemeFolder, Slash));
            }

            return source;
        }

        private void SetThemeFolder(string themeFolder)
        {
            ThemeFolder = themeFolder;
        }

        public static IThemeFilePathManipulator CreateNewThemeFilePathManipulator()
        {
            return new ThemeFilePathManipulator();
        }

        public static IThemeFilePathManipulator CreateNewThemeFilePathManipulator(string themeFolder)
        {
            return new ThemeFilePathManipulator(themeFolder);
        }
    }
}

But shouldn’t we see if the file exists, and then return the path if it does, or the original path if it does not? This question lead to the creation of the following decorator class to do just that:

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.Detectors.Base;
using Sitecore.Sandbox.Utilities.Manipulators.Base;

namespace Sitecore.Sandbox.Utilities.Manipulators
{
    public class ThemeFilePathIfExistsManipulator : IThemeFilePathManipulator
    {
        private IThemeFilePathManipulator InnerManipulator { get; set; }
        private IStringDetector FileExistsDetector { get; set; }

        private string _ThemeFolder;
        public string ThemeFolder 
        {
            get
            {
                return InnerManipulator.ThemeFolder;
            }
            set
            {
                InnerManipulator.ThemeFolder = value;
            }
        }

        private ThemeFilePathIfExistsManipulator(IThemeFilePathManipulator innerManipulator, IStringDetector fileExistsDetector)
        {
            SetInnerManipulator(innerManipulator);
            SetFileExistsDetector(fileExistsDetector);
        }

        private void SetInnerManipulator(IThemeFilePathManipulator innerManipulator)
        {
            Assert.ArgumentNotNull(innerManipulator, "innerManipulator");
            InnerManipulator = innerManipulator;
        }

        private void SetFileExistsDetector(IStringDetector fileExistsDetector)
        {
            Assert.ArgumentNotNull(fileExistsDetector, "fileExistsDetector");
            FileExistsDetector = fileExistsDetector;
        }

        public string Manipulate(string source)
        {
            Assert.ArgumentNotNullOrEmpty(source, "source");
            string filePath = InnerManipulator.Manipulate(source);
            if(!DoesFileExist(filePath))
            {
                return source;
            }

            return filePath;
        }

        private bool DoesFileExist(string filePath)
        {
            FileExistsDetector.Source = filePath;
            return FileExistsDetector.Detect();
        }

        private void SetThemeFolder(string themeFolder)
        {
            ThemeFolder = themeFolder;
        }

        public static IThemeFilePathManipulator CreateNewThemeFilePathManipulator(IThemeFilePathManipulator innerManipulator, IStringDetector fileExistsDetector)
        {
            return new ThemeFilePathIfExistsManipulator(innerManipulator, fileExistsDetector);
        }
    }
}

Next, I defined a contract for manipulators that transform instances of Sitecore.Layouts.RenderingReference:

using Sitecore.Layouts;

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IRenderingReferenceManipulator : IManipulator<RenderingReference>
    {
    }
}

I then created a class to manipulate instances of Sitecore.Layouts.RenderingReference by swapping out their sublayouts with new instances based on the path returned by the IThemeFilePathManipulator instance:

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IThemeRenderingReferenceManipulator : IRenderingReferenceManipulator, IThemeFolder
    {
    }
}
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Layouts;
using Sitecore.Web.UI.WebControls;

using Sitecore.Sandbox.Utilities.Detectors.Base;
using Sitecore.Sandbox.Utilities.Manipulators.Base;

namespace Sitecore.Sandbox.Utilities.Manipulators
{
    public class ThemeRenderingReferenceManipulator : IThemeRenderingReferenceManipulator
    {
        private IItemDetector SublayoutDetector { get; set; }
        private IThemeFilePathManipulator ThemeFilePathManipulator { get; set; }

        public string ThemeFolder
        {
            get
            {
                return ThemeFilePathManipulator.ThemeFolder;
            }
            set
            {
                ThemeFilePathManipulator.ThemeFolder = value;
            }
        }

        private ThemeRenderingReferenceManipulator()
            : this(CreateDefaultThemeFilePathManipulator())
        {
        }

        private ThemeRenderingReferenceManipulator(IThemeFilePathManipulator themeFilePathManipulator)
            : this(CreateDefaultSublayoutDetector(), themeFilePathManipulator)
        {
        }

        private ThemeRenderingReferenceManipulator(IItemDetector sublayoutDetector, IThemeFilePathManipulator themeFilePathManipulator)
        {
            SetSublayoutDetector(sublayoutDetector);
            SetThemeFilePathManipulator(themeFilePathManipulator);
        }

        private void SetSublayoutDetector(IItemDetector sublayoutDetector)
        {
            Assert.ArgumentNotNull(sublayoutDetector, "sublayoutDetector");
            SublayoutDetector = sublayoutDetector;
        }

        private void SetThemeFilePathManipulator(IThemeFilePathManipulator themeFilePathManipulator)
        {
            Assert.ArgumentNotNull(themeFilePathManipulator, "themeFilePathManipulator");
            ThemeFilePathManipulator = themeFilePathManipulator;
        }

        public RenderingReference Manipulate(RenderingReference source)
        {
            Assert.ArgumentNotNull(source, "source");
            Assert.ArgumentNotNull(source.RenderingItem, "source.RenderingItem");
            
            if (!IsSublayout(source.RenderingItem.InnerItem))
            {
                return source; 
            }

            RenderingReference renderingReference = source;
            Sublayout sublayout = source.GetControl() as Sublayout;
            
            if (sublayout == null)
            {
                return source;
            }

            renderingReference.SetControl(CreateNewSublayout(sublayout, ThemeFilePathManipulator.Manipulate(sublayout.Path)));
            return renderingReference;
        }

        private bool IsSublayout(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            SublayoutDetector.Source = item;
            return SublayoutDetector.Detect();
        }

        private static Sublayout CreateNewSublayout(Sublayout sublayout, string path)
        {
            Assert.ArgumentNotNull(sublayout, "sublayout");
            Assert.ArgumentNotNullOrEmpty(path, "path");
            return new Sublayout
            {
                ID = sublayout.ID,
                Path = path,
                Background = sublayout.Background,
                Border = sublayout.Border,
                Bordered = sublayout.Bordered,
                Cacheable = sublayout.Cacheable,
                CacheKey = sublayout.CacheKey,
                CacheTimeout = sublayout.CacheTimeout,
                Clear = sublayout.Clear,
                CssStyle = sublayout.CssStyle,
                Cursor = sublayout.Cursor,
                Database = sublayout.Database,
                DataSource = sublayout.DataSource,
                DisableDebug = sublayout.DisableDebug,
                DisableDots = sublayout.DisableDots,
                DisableSecurity = sublayout.DisableSecurity,
                Float = sublayout.Float,
                Foreground = sublayout.Foreground,
                LiveDisplay = sublayout.LiveDisplay,
                LoginRendering = sublayout.LoginRendering,
                Margin = sublayout.Margin,
                Padding = sublayout.Padding,
                Parameters = sublayout.Parameters,
                RenderAs = sublayout.RenderAs,
                RenderingID = sublayout.RenderingID,
                RenderingName = sublayout.RenderingName,
                VaryByData = sublayout.VaryByData,
                VaryByDevice = sublayout.VaryByDevice,
                VaryByLogin = sublayout.VaryByLogin,
                VaryByParm = sublayout.VaryByParm,
                VaryByQueryString = sublayout.VaryByQueryString,
                VaryByUser = sublayout.VaryByUser
            };
        }

        private static IThemeFilePathManipulator CreateDefaultThemeFilePathManipulator()
        {
            return Manipulators.ThemeFilePathManipulator.CreateNewThemeFilePathManipulator();
        }

        private static IItemDetector CreateDefaultSublayoutDetector()
        {
            return Utilities.Detectors.SublayoutDetector.CreateNewSublayoutDetector();
        }

        public static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator()
        {
            return new ThemeRenderingReferenceManipulator();
        }

        public static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator(IThemeFilePathManipulator themeFilePathManipulator)
        {
            return new ThemeRenderingReferenceManipulator(themeFilePathManipulator);
        }
        
        public static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator(IItemDetector sublayoutDetector, IThemeFilePathManipulator themeFilePathManipulator)
        {
            return new ThemeRenderingReferenceManipulator(sublayoutDetector, themeFilePathManipulator);
        }
    }
}

I created a new httpRequestBegin pipeline processor to use a theme’s layout if found on the file system — such is done by delegating to instances of classes defined above:

using System;
using System.Web;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.HttpRequest;

using Sitecore.Sandbox.Utilities.Manipulators;
using Sitecore.Sandbox.Utilities.Manipulators.Base;
using Sitecore.Sandbox.Utilities.Detectors;
using Sitecore.Sandbox.Utilities.Detectors.Base;

namespace Sitecore.Sandbox.Pipelines.HttpRequest
{
    public class ThemeLayoutResolver : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            if (!CanProcess())
            {
                return;
            }

            string themeFilePath = GetThemeFilePath();
            bool swapOutFilePath = !AreEqualIgnoreCase(Context.Page.FilePath, themeFilePath);
            if (swapOutFilePath)
            {
                Context.Page.FilePath = themeFilePath;
            }
        }

        private static bool CanProcess()
        {
            return Context.Database != null
                    && !IsCore(Context.Database);
        }

        private static bool IsCore(Database database)
        {
            return database.Name == Constants.CoreDatabaseName;
        }

        private static string GetThemeFilePath()
        {
            IThemeFilePathManipulator themeFilePathManipulator = CreateNewThemeFilePathManipulator();
            themeFilePathManipulator.ThemeFolder = GetTheme();
            return themeFilePathManipulator.Manipulate(Context.Page.FilePath);
        }

        private static bool DoesFileExist(string filePath)
        {
            Assert.ArgumentNotNullOrEmpty(filePath, "filePath");
            IStringDetector fileExistsDetector = CreateNewServerFileExistsDetector();
            fileExistsDetector.Source = filePath;
            return fileExistsDetector.Detect();
        }

        private static IThemeFilePathManipulator CreateNewThemeFilePathManipulator()
        {
            return ThemeFilePathIfExistsManipulator.CreateNewThemeFilePathManipulator
            (
                ThemeFilePathManipulator.CreateNewThemeFilePathManipulator(),
                CreateNewServerFileExistsDetector()
            );
        }

        private static IStringDetector CreateNewServerFileExistsDetector()
        {
            IStringDetectorFactory factory = StringDetectorFactory.CreateNewStringDetectorFactory();
            return factory.CreateNewServerFileExistsDetector(HttpContext.Current.Server);
        }

        private static string GetTheme()
        {
            Item home = GetHomeItem();
            return home["Theme"];
        }

        private static Item GetHomeItem()
        {
            string startPath = Factory.GetSite(Sitecore.Context.GetSiteName()).StartPath;
            return Context.Database.GetItem(startPath);
        }

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

The above code should not run if the context database is the core database — this could mean we are in the Sitecore shell, or on the Sitecore login page (I painfully discovered this earlier today after doing a QA drop, and had to act fast to fix it).

I then created a RenderLayout pipeline processor to swap out sublayouts for themed sublayouts if they exist on the file system:

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

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Layouts;
using Sitecore.Pipelines;
using Sitecore.Pipelines.InsertRenderings;
using Sitecore.Pipelines.RenderLayout;

using Sitecore.Sandbox.Utilities.Manipulators;
using Sitecore.Sandbox.Utilities.Manipulators.Base;
using Sitecore.Sandbox.Utilities.Detectors;
using Sitecore.Sandbox.Utilities.Detectors.Base;

namespace Sitecore.Sandbox.Pipelines.RenderLayout
{
    public class InsertThemeRenderings : RenderLayoutProcessor
    {
        public override void Process(RenderLayoutArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (Context.Item == null)
            {
                return;
            }
            
            using (new ProfileSection("Insert renderings into page."))
            {
                IEnumerable<RenderingReference> themeRenderingReferences = GetThemeRenderingReferences(GetRenderingReferences());
                foreach (RenderingReference renderingReference in themeRenderingReferences)
                {
                    Context.Page.AddRendering(renderingReference);
                }
            }
        }

        private static IEnumerable<RenderingReference> GetRenderingReferences()
        {
            InsertRenderingsArgs insertRenderingsArgs = new InsertRenderingsArgs();
            CorePipeline.Run("insertRenderings", insertRenderingsArgs);
            return insertRenderingsArgs.Renderings.ToList();
        }

        private static IEnumerable<RenderingReference> GetThemeRenderingReferences(IEnumerable<RenderingReference> renderingReferences)
        {
            Assert.ArgumentNotNull(renderingReferences, "renderingReferences");
            IList<RenderingReference> themeRenderingReferences = new List<RenderingReference>();
            IThemeRenderingReferenceManipulator themeRenderingReferenceManipulator = CreateNewRenderingReferenceManipulator();
            foreach (RenderingReference renderingReference in renderingReferences)
            {
                themeRenderingReferences.Add(themeRenderingReferenceManipulator.Manipulate(renderingReference));
            }

            return themeRenderingReferences;
        }

        private static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator()
        {
            IThemeRenderingReferenceManipulator themeRenderingReferenceManipulator = ThemeRenderingReferenceManipulator.CreateNewRenderingReferenceManipulator(CreateNewThemeFilePathManipulator());
            themeRenderingReferenceManipulator.ThemeFolder = GetTheme();
            return themeRenderingReferenceManipulator;
        }

        private static IThemeFilePathManipulator CreateNewThemeFilePathManipulator()
        {
            return ThemeFilePathIfExistsManipulator.CreateNewThemeFilePathManipulator
            (
                ThemeFilePathManipulator.CreateNewThemeFilePathManipulator(),
                CreateNewServerFileExistsDetector()
            );
        }

        private static IStringDetector CreateNewServerFileExistsDetector()
        {
            IStringDetectorFactory factory = StringDetectorFactory.CreateNewStringDetectorFactory();
            return factory.CreateNewServerFileExistsDetector(HttpContext.Current.Server);
        }

        private static string GetTheme()
        {
            Item home = GetHomeItem();
            return home["Theme"];
        }

        private static Item GetHomeItem()
        {
            string startPath = Factory.GetSite(Sitecore.Context.GetSiteName()).StartPath;
            return Context.Database.GetItem(startPath);
        }
    }
}

I baked everything together in a patch include configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderLayout>
        <processor patch:instead="processor[@type='Sitecore.Pipelines.RenderLayout.InsertRenderings, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.RenderLayout.InsertThemeRenderings, Sitecore.Sandbox" />
      </renderLayout>
      <httpRequestBegin>
        <processor patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.HttpRequest.ThemeLayoutResolver, Sitecore.Sandbox"/>
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

Here are the “out of the box” presentation components mapped to my test page:

theme-sublayouts-rendering-presentation-details

The above layout and sublayouts live in /layouts from my website root.

For my themes, I duplicated the layout and some of the sublayouts above into themed folders, and added some css for color — I’ve omitted this for brevity (come on Mike, this post is already wicked long :) ).

The “Blue Theme” layout and sublayout live in /layouts/Blue Theme from my website root:

blue-theme-files

I chose “Blue Theme” in the Theme droplist on my home item, published, and then navigated to my test page:

blue-theme-page

The “Inverse Blue Theme” sublayout lives in /layouts/Inverse Blue Theme from my website root:

inverse-blue-theme-files

I chose “Inverse Blue Theme” in the Theme droplist on my home item, published, and then reloaded my test page:

inverse-blue-theme-page

The “Green Theme” layout and sublayouts live in /layouts/Green Theme from my website root:

green-theme-files

I chose “Green Theme” in the Theme droplist on my home item, published, and then refreshed my test page:

green-theme-page

Do you know of or have an idea for another solution to accomplish the same? If so, please drop a comment.


Suppress Most Sitecore Wizard Cancel Confirmation Prompts

$
0
0

By default, clicking the ‘Cancel’ button on most wizard forms in Sitecore yields the following confirmation dialog:

ConfirmNoSuppress

Have you ever said to yourself “Yes, I’m sure I’m sure” after seeing this, and wondered if there were a setting you could toggle to turn it off?

Earlier today, while surfing through my Web.config, the closeWizard client pipeline — located at /sitecore/processors/closeWizard in the Web.config — had caught my eye, and I was taken aback over how I had not noticed it before. I was immediately curious over what gems I might find within its only processor — /sitecore/processors/closeWizard/processor[@type='Sitecore.Web.UI.Pages.WizardForm, Sitecore.Kernel' and @method='Confirmation'] — and whether there would be any utility in overriding/extending it.

At first, I thought having a closeWizard client pipeline processor to completely suppress the “Are you sure you want to close the wizard?” confirmation prompt would be ideal, but then imagined how irate someone might be after clicking the ‘Cancel’ button by accident, which would result in the loss of his/her work.

As a happy medium between always prompting users whether they are certain they want to close their wizards and not prompting at all, I came up with the following closeWizard client pipeline processor:

using System;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Pages;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Web.UI.Pages
{
    public class SuppressConfirmationWizardForm : WizardForm
    {
        private const string SuppressConfirmationRegistryKey = "/Current_User/Content Editor/Suppress Close Wizard Confirmation";

        private static string YesNoCancelDialogPrompt { get; set; }
        private static int YesNoCancelDialogWidth { get; set; }
        private static int YesNoCancelDialogHeight { get; set; }

        static SuppressConfirmationWizardForm()
        {
            YesNoCancelDialogPrompt = Settings.GetSetting("SuppressConfirmationYesNoCancelDialog.Prompt");
            YesNoCancelDialogWidth = Settings.GetIntSetting("SuppressConfirmationYesNoCancelDialog.Width", 100);
            YesNoCancelDialogHeight = Settings.GetIntSetting("SuppressConfirmationYesNoCancelDialog.Height", 100);
        }

        public void CloseWizard(ClientPipelineArgs args)
        {
            if (IsCancel(args))
            {
                args.AbortPipeline();
                return;
            }

            if (ShouldSaveShouldSuppressConfirmationSetting(args))
            {
                SaveShouldSuppressConfirmationSetting(args);
            }
            
            if (ShouldCloseWizard(args))
            {
                EndWizard();
            }
            else
            {
                SheerResponse.YesNoCancel(YesNoCancelDialogPrompt, YesNoCancelDialogWidth.ToString(), YesNoCancelDialogHeight.ToString());
                args.WaitForPostBack();
            }
        }

        private static bool ShouldCloseWizard(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            return args.HasResult || ShouldSuppressConfirmationSetting();
        }

        private static bool ShouldSuppressConfirmationSetting()
        {
            return Registry.GetBool(SuppressConfirmationRegistryKey);
        }

        private static bool ShouldSaveShouldSuppressConfirmationSetting(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            return args.HasResult && !IsCancel(args);
        }

        private static void SaveShouldSuppressConfirmationSetting(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Registry.SetBool(SuppressConfirmationRegistryKey, AreEqualIgnoreCase(args.Result, "yes"));
        }

        private static bool IsCancel(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            return AreEqualIgnoreCase(args.Result, "cancel");
        }

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

The pipeline processor above will let users decide whether they want to continue seeing the “Are you sure?”‘ confirmation prompt — albeit I had to change the messaging to something more fitting giving the new functionality (see the patch include configuration file or testing screenshot below for the new messaging).

If a user clicks ‘Yes’, s/he will never be prompted with this dialog again — this preference is saved in a Sitecore registry setting for the user.

Plus, suppressing this dialog in one place will suppress it everywhere it would display

Clicking ‘No’ will ensure the message is displayed again in the future.

Clicking ‘Cancel’ will just close the confirmation dialog, and return the user back to the wizard.

You might be wondering why I subclassed Sitecore.Web.UI.Pages.WizardForm. I had to do this in order to get access to its EndWizard() method which is a protected method. This method closes the wizard form.

I plugged it all in via a patch include configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <closeWizard>
        <processor mode="on" patch:instead="processor[@type='Sitecore.Web.UI.Pages.WizardForm, Sitecore.Kernel' and @method='Confirmation']" type="Sitecore.Sandbox.Web.UI.Pages.SuppressConfirmationWizardForm, Sitecore.Sandbox" method="CloseWizard"/>
      </closeWizard>
    </processors>
    <settings>
      <setting name="SuppressConfirmationYesNoCancelDialog.Prompt" value="You are about to close the wizard. Would you also like to avoid this message in the future?" />
      <setting name="SuppressConfirmationYesNoCancelDialog.Width" value="100" />
      <setting name="SuppressConfirmationYesNoCancelDialog.Height" value="100" />
    </settings>
  </sitecore>
</configuration>

I tested the above pipeline processor on the wizard for creating a new data template:

YesNoCancelSuppressConfirmation

I decided to omit screenshots after clicking ‘Yes’, ‘No’ and ‘Cancel’ — there really isn’t much to show since all close the confirmation dialog, with the ‘Yes’ and ‘No’ buttons also closing the wizard.

I also did a little research to see what wizard forms in Sitecore might be impacted by the above, and compiled the following list of wizard form classes — this list contains classes from both Sitecore.Kernel.dll and Sitecore.Client.dll:

  • Sitecore.Shell.Applications.Analytics.Lookups.RunLookupForm
  • Sitecore.Shell.Applications.Analytics.Reports.Summary.UpdateForm
  • Sitecore.Shell.Applications.Analytics.SynchronizeDatabase.SynchronizeDatabaseForm
  • Sitecore.Shell.Applications.Analytics.VisitorIdentifications.RunVisitorIdentificationsForm
  • Sitecore.Shell.Applications.Databases.CleanUp.CleanUpForm
  • Sitecore.Shell.Applications.Dialogs.ArchiveDate.ArchiveDateForm
  • Sitecore.Shell.Applications.Dialogs.FixLinks.FixLinksForm
  • Sitecore.Shell.Applications.Dialogs.Publish.PublishForm
  • Sitecore.Shell.Applications.Dialogs.RebuildLinkDatabase.RebuildLinkDatabaseForm
  • Sitecore.Shell.Applications.Dialogs.Reminder.ReminderForm
  • Sitecore.Shell.Applications.Dialogs.TransferToDatabase.TransferToDatabaseForm
  • Sitecore.Shell.Applications.Dialogs.Upload.UploadForm
  • Sitecore.Shell.Applications.Globalization.AddLanguage.AddLanguageForm
  • Sitecore.Shell.Applications.Globalization.DeleteLanguage.DeleteLanguageForm
  • Sitecore.Shell.Applications.Globalization.ExportLanguage.ExportLanguageForm
  • Sitecore.Shell.Applications.Globalization.ImportLanguage.ImportLanguageForm
  • Sitecore.Shell.Applications.Globalization.UntranslatedFields.UntranslatedFieldsForm
  • Sitecore.Shell.Applications.Install.Dialogs.AddFileSourceForm
  • Sitecore.Shell.Applications.Install.Dialogs.AddItemSourceForm
  • Sitecore.Shell.Applications.Install.Dialogs.AddStaticFileSourceDialog
  • Sitecore.Shell.Applications.Install.Dialogs.AddStaticItemSourceDialog
  • Sitecore.Shell.Applications.Install.Dialogs.BuildPackage
  • Sitecore.Shell.Applications.Install.Dialogs.InstallPackage.InstallPackageForm
  • Sitecore.Shell.Applications.Install.Dialogs.UploadPackageForm
  • Sitecore.Shell.Applications.Layouts.IDE.Wizards.NewFileWizard.IDENewFileWizardForm
  • Sitecore.Shell.Applications.Layouts.IDE.Wizards.NewMethodRenderingWizard.IDENewMethodRenderingWizardForm
  • Sitecore.Shell.Applications.Layouts.IDE.Wizards.NewUrlRenderingWizard.IDENewUrlRenderingWizardForm
  • Sitecore.Shell.Applications.Layouts.IDE.Wizards.NewWebControlWizard.IDENewWebControlWizardForm
  • Sitecore.Shell.Applications.Layouts.Layouter.Wizards.NewLayout.NewLayoutForm
  • Sitecore.Shell.Applications.Layouts.Layouter.Wizards.NewSublayout.NewSublayoutForm
  • Sitecore.Shell.Applications.Layouts.Layouter.Wizards.NewXMLLayout.NewXMLLayoutForm
  • Sitecore.Shell.Applications.Layouts.Layouter.Wizards.NewXSL.NewXSLForm
  • Sitecore.Shell.Applications.MarketingAutomation.Dialogs.ForceTriggerForm
  • Sitecore.Shell.Applications.MarketingAutomation.Dialogs.ImportVisitorsForm
  • Sitecore.Shell.Applications.ScheduledTasks.NewSchedule.NewScheduleForm
  • Sitecore.Shell.Applications.ScheduledTasks.NewScheduleCommand.NewScheduleCommandForm
  • Sitecore.Shell.Applications.Search.RebuildSearchIndex.RebuildSearchIndexForm
  • Sitecore.Shell.Applications.Templates.ChangeTemplate.ChangeTemplateForm
  • Sitecore.Shell.Applications.Templates.CreateTemplate.CreateTemplateForm

If you can think of any other ways of customizing this client pipeline, please drop a comment.


Display Content Management Server Information in the Sitecore CMS

$
0
0

The other day I cogitated over potential uses for the getAboutInformation pipeline. Found at /configuration/sitecore/pipelines/getAboutInformation in the Web.config, it can be leveraged to display information on the Sitecore login page, and inside of the About dialog — a dialog that can be launched from the Content Editor.

One thing that came to mind was displaying some information for the Content Management (CM) server where the Sitecore instance lives. Having this information readily available might aid in troubleshooting issues that arise, or seeing the name of the server might stop you from making content changes on the wrong CM server (I am guilty as charged for committing such a blunder in the past).

This post shows how I translated that idea into code.

The first thing we need is a way to get server information. I defined the following interface to describe information we might be interested in for a server:

namespace Sitecore.Sandbox.Utilities.Server.Base
{
    public interface IServer
    {
        string Name { get; }

        string Cpu { get; }

        string OperatingSystem { get; }
    }
}

We now need a class to implement the above interface. I stumbled upon a page whose author shared how one can acquire server information using classes defined in the System.Management namespace in .NET.

Using information from that page coupled with some experimentation, I came up with the following class:

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

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.Server.Base;

namespace Sitecore.Sandbox.Utilities.Server
{
    public class Server : IServer
    {
        private string _Name;
        public string Name
        {
            get
            {
                if (string.IsNullOrEmpty(_Name))
                {
                    _Name = GetServerName();
                }

                return _Name;
            }
        }

        private string _Cpu;
        public string Cpu
        {
            get
            {
                if (string.IsNullOrEmpty(_Cpu))
                {
                    _Cpu = GetCpuInformation();
                }

                return _Cpu;
            }
        }
        
        private string _OperatingSystem;
        public string OperatingSystem
        {
            get
            {
                if (string.IsNullOrEmpty(_OperatingSystem))
                {
                    _OperatingSystem = GetOperatingSystemName();
                }

                return _OperatingSystem;
            }
        }

        private Server()
        {
        }

        private static string GetServerName()
        {
            return GetFirstManagementBaseObjectPropertyFirstInnerProperty("Win32_ComputerSystem", "name");
        }

        private static string GetCpuInformation()
        {
            return GetFirstManagementBaseObjectPropertyFirstInnerProperty("Win32_Processor", "name");
        }

        private static string GetOperatingSystemName()
        {
            return GetFirstManagementBaseObjectPropertyFirstInnerProperty("Win32_OperatingSystem", "name");
        }

        private static string GetFirstManagementBaseObjectPropertyFirstInnerProperty(string key, string propertyName)
        {
            return GetFirstManagementBaseObjectPropertyInnerProperties(key, propertyName).FirstOrDefault();
        }

        private static IEnumerable<string> GetFirstManagementBaseObjectPropertyInnerProperties(string key, string propertyName)
        {
            return GetFirstManagementBaseObjectProperty(key, propertyName).Split('|');
        }

        private static string GetFirstManagementBaseObjectProperty(string key, string propertyName)
        {
            return GetFirstManagementBaseObject(key)[propertyName].ToString();
        }

        private static ManagementBaseObject GetFirstManagementBaseObject(string key)
        {
            Assert.ArgumentNotNullOrEmpty(key, "key");
            WqlObjectQuery query = new WqlObjectQuery(string.Format("select * from {0}", key));
            ManagementObjectSearcher searcher = new ManagementObjectSearcher(query);
            return searcher.Get().Cast<ManagementBaseObject>().FirstOrDefault();
        }

        public static IServer CreateNewServer()
        {
            return new Server();
        }
    }
}

The class above grabs server information via three separate ManagementObjectSearcher queries, one for each property defined in our IServer interface.

In order to use classes defined in the System.Management namespace, I had to reference System.Management in my project in Visual Studio:

system-management-reference

Next, I created a class that contains methods that will serve as our getAboutInformation pipeline processors:

using System.Collections.Generic;

using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetAboutInformation;

using Sitecore.Sandbox.Utilities.Server.Base;
using Sitecore.Sandbox.Utilities.Server;

namespace Sitecore.Sandbox.Pipelines.GetAboutInformation
{
    public class GetContentManagementServerInformation
    {
        private static readonly string CurrentServerInformationHtml = GetCurrentServerInformationHtml();

        public void SetLoginPageText(GetAboutInformationArgs args)
        {
            args.LoginPageText = CurrentServerInformationHtml;
        }

        public void SetAboutText(GetAboutInformationArgs args)
        {
            args.AboutText = CurrentServerInformationHtml;
        }

        private static string GetCurrentServerInformationHtml()
        {
            return GetServerInformationHtml(Server.CreateNewServer());
        }

        private static string GetServerInformationHtml(IServer server)
        {
            Assert.ArgumentNotNull(server, "server");
            IList<string> information = new List<string>();

            if (!string.IsNullOrEmpty(server.Name))
            {
                information.Add(string.Format("<strong>Server Name</strong>: {0}", server.Name));
            }

            if (!string.IsNullOrEmpty(server.Cpu))
            {
                information.Add(string.Format("<strong>CPU</strong>: {0}", server.Cpu));
            }

            if (!string.IsNullOrEmpty(server.OperatingSystem))
            {
                information.Add(string.Format("<strong>OS</strong>: {0}", server.OperatingSystem));
            }

            return string.Join("<br />", information);
        }
    }
}

Both methods set properties on the GetAboutInformationArgs instance using the same HTML generated by the GetServerInformationHtml method. This method is given an instance of the Server class defined above by the GetCurrentServerInformationHtml method.

I then connected all of the above into Sitecore via a configuration include file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getAboutInformation>
        <processor type="Sitecore.Sandbox.Pipelines.GetAboutInformation.GetContentManagementServerInformation, Sitecore.Sandbox" method="SetLoginPageText" />
        <processor type="Sitecore.Sandbox.Pipelines.GetAboutInformation.GetContentManagementServerInformation, Sitecore.Sandbox" method="SetAboutText" />
      </getAboutInformation>
    </pipelines>
  </sitecore>
</configuration>

Let’s see this in action.

When hitting the Sitecore login page in my browser, I saw server information in the right sidebar, under the Sitecore version and revision numbers:

login-page-server-information

Next, I logged into Sitecore, opened the Content Editor, and launched the About dialog:

about-dialog-server-information

As you can see, my CM server information is also displayed here.

You might be questioning why I didn’t include more server information on both the login page and About dialog. One reason why I omitted displaying other properties is due to discovering that the login page area for showing the LoginPageText string does not grow vertically — I saw this when I did include a few more properties in addition to the three shown above.

Sadly, I did not see what would happen when including these additional properties in the the About dialog. Ascertaining whether it is possible to include more information in the About dialog is warranted, though I will leave that exercise for another day.

If you have any other thoughts or ideas for utilizing getAboutInformation pipeline processors, or other areas in Sitecore where server information might be useful, please drop a comment.


Add Additional Item Properties in Sitecore Item Web API Responses

$
0
0

The other day I was exploring pipelines of the Sitecore Item Web API, and took note of the itemWebApiGetProperties pipeline. This pipeline adds information about an item in the response returned by the Sitecore Item Web API. You can find this pipeline at /configuration/sitecore/pipelines/itemWebApiGetProperties in \App_Config\Include\Sitecore.ItemWebApi.config.

The following properties are set for an item in the response via the lonely pipeline processor — /configuration/sitecore/pipelines/itemWebApiGetProperties/processor[@type="Sitecore.ItemWebApi.Pipelines.GetProperties.GetProperties, Sitecore.ItemWebApi"] — that ships with the Sitecore Item Web API:

out-of-the-box-properties

Here’s an example of what the properties set by the above pipeline processor look like in the response — I invoked a read request to the Sitecore Item Web API via a copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK:

properties-out-of-box-console

You might be asking “how difficult would it be to add in my own properties?” It’s not difficult at all!

I whipped up the following itemWebApiGetProperties pipeline processor to show how one can add more properties for an item:

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.GetProperties;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.GetProperties
{
    public class GetEvenMoreProperties : GetPropertiesProcessor
    {
        public override void Process(GetPropertiesArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            arguments.Properties.Add("ParentID", arguments.Item.ParentID.ToString());
            arguments.Properties.Add("ChildrenCount", arguments.Item.Children.Count);
            arguments.Properties.Add("Level", arguments.Item.Axes.Level);
            arguments.Properties.Add("IsItemClone", arguments.Item.IsItemClone);
            arguments.Properties.Add("CreatedBy", arguments.Item["__Created by"]);
            arguments.Properties.Add("UpdatedBy", GetItemUpdatedBy(arguments.Item));
        }

        private static Dynamic GetItemUpdatedBy(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            string[] usernamePieces = item["__Updated by"].Split('\\');

            Dynamic username = new Dynamic();
            if (usernamePieces.Length > 1)
            {
                username["Domain"] = usernamePieces[0];
                username["Username"] = usernamePieces[1];
            }
            else if (usernamePieces.Length > 0)
            {
                username["Username"] = usernamePieces[0];
            }

            return username;
        }
    }
}

The ParentID, ChildrenCount, Level and IsItemClone properties are simply added to the properties SortedDictionary within the GetPropertiesArgs instance, and will be serialized as is.

For the UpdatedBy property, I decided to leverage the Sitecore.ItemWebApi.Dynamic class in order to have the username set in the “__Updated by” field be represented by a JSON object. This JSON object sets the domain and username — without the domain — into different JSON properties.

As a side note — when writing your own service code for the Sitecore Item Web API — I strongly recommend using instances of the Sitecore.ItemWebApi.Dynamic class — or something similar — for complex objects. Developers writing code to consume your JSON will thank you many times for it. :)

I registered my new processor to the itemWebApiGetProperties pipeline in my Sitecore instance’s \App_Config\Include\Sitecore.ItemWebApi.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- there's stuff here -->
      <itemWebApiGetProperties>
        <processor type="Sitecore.ItemWebApi.Pipelines.GetProperties.GetProperties, Sitecore.ItemWebApi" />
        <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.GetProperties.GetEvenMoreProperties, Sitecore.Sandbox" />
      </itemWebApiGetProperties>
      <!-- there's stuff here as well -->
  </sitecore>
</configuration>

Let’s take this for a spin.

I ran the console application again to see what the response now looks like:

additional-properties

As you can see, our additional properties are now included in the response.

If you can think of other item properties that would be useful for Sitecore Item Web API client applications, please share in a comment.

Until next time, have a Sitecorelicious day!


Go Green: Put Items in the Recycle Bin When Deleting Via the Sitecore Item Web API

$
0
0

This morning I discovered that items are permanently deleted by the Sitecore Item Web API during a delete action. This is probably called out somewhere in its developer’s guide but I don’t recall having read this.

Regardless of whether it’s highlighted somewhere in documentation, I decided to investigate why this happens.

After combing through Sitecore Item Web API pipelines defined in \App_Config\Include\Sitecore.ItemWebApi.config and code in Sitecore.ItemWebApi.dll, I honed in on the following:

delete-scope-bye-bye

This above code lives in the only itemWebApiDelete pipeline processor that comes with the Sitecore Item Web API, and this processor can be found at /configuration/sitecore/pipelines/itemWebApiDelete/processor[@type="Sitecore.ItemWebApi.Pipelines.Delete.DeleteScope, Sitecore.ItemWebApi"] in the \App_Config\Include\Sitecore.ItemWebApi.config file.

I don’t know about you, but I’m not always comfortable with deleting items permanently in Sitecore. I heavily rely on Sitecore’s Recycle Bin — yes, I have deleted items erroneously in the past, but recovered quickly by restoring them from the Recycle Bin (I hope I’m not the only one who has done this. :-/)

Unearthing the above prompted me to write a new itemWebApiDelete pipeline processor that puts items in the Recycle Bin when the Recycle Bin setting — see /configuration/sitecore/settings/setting[@name="RecycleBinActive"] in the Web.config — is enabled:

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

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Delete;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Delete
{
    public class RecycleScope : DeleteProcessor
    {
        private const int OKStatusCode = 200;

        public override void Process(DeleteArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            IEnumerable<Item> itemsToDelete = arguments.Scope;
            DeleteItems(itemsToDelete);
            arguments.Result = GetStatusInformation(OKStatusCode, GetDeletionInformation(itemsToDelete));
        }

        private static void DeleteItems(IEnumerable<Item> itemsToDelete)
        {
            foreach (Item itemToDelete in itemsToDelete)
            {
                DeleteItem(itemToDelete);
            }
        }

        private static void DeleteItem(Item itemToDelete)
        {
            Assert.ArgumentNotNull(itemToDelete, "itemToDelete");

            // put items in the recycle bin if it's turned on
            if (Settings.RecycleBinActive)
            {
                itemToDelete.Recycle();
            }
            else
            {
                itemToDelete.Delete();
            }
        }

        private static Dynamic GetDeletionInformation(IEnumerable<Item> itemsToDelete)
        {
            return GetDeletionInformation(itemsToDelete.Count(), GetItemIds(itemsToDelete));
        }

        private static Dynamic GetDeletionInformation(int count, IEnumerable<ID> itemIds)
        {
            Dynamic deletionInformation = new Dynamic();
            deletionInformation["count"] = count;
            deletionInformation["itemIds"] = itemIds.Select(id => id.ToString());
            return deletionInformation;
        }

        private static IEnumerable<ID> GetItemIds(IEnumerable<Item> items)
        {
            Assert.ArgumentNotNull(items, "items");
            return items.Select(item => item.ID);
        }

        private static Dynamic GetStatusInformation(int statusCode, Dynamic result)
        {
            Assert.ArgumentNotNull(result, "result");
            Dynamic status = new Dynamic();
            status["statusCode"] = statusCode;
            status["result"] = result;
            return status;
        }
    }
}

There really isn’t anything magical about the code above. It utilizes most of the same logic that comes with the itemWebApiDelete pipeline processor that ships with the Sitecore Item Web API, although I did move code around into new methods.

The only major difference is the invocation of the Recycle method on item instances when the Recycle Bin is enabled in Sitecore. If the Recycle Bin is not enabled, we call the Delete method instead — as does the “out of the box” pipeline processor.

I then replaced the existing itemWebApiDelete pipeline processor in \App_Config\Include\Sitecore.ItemWebApi.config with our new one defined above:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- stuff is defined up here -->
      <itemWebApiDelete>
        <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Delete.RecycleScope, Sitecore.Sandbox" />
	  </itemWebApiDelete>
      <!-- there's more stuff defined down here -->
  </sitecore>
</configuration>

Let’s see this in action.

We first need a test item. Let’s create one together:

recycle-item-web-api-delete-test-item

I then tweaked the delete method in my copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK, to point to our test item in the master database — I have omitted this code for the sake of brevity — and then ran the console application calling the delete method only:

test-item-delete-console-response

As you can see, our test item is now in the Recycle Bin:

test-item-recycle-bin

If you have any thoughts on this, please leave a comment.


Enforce Password Expiration in the Sitecore CMS

$
0
0

I recently worked on a project that called for a feature to expire Sitecore users’ passwords after an elapsed period of time since their passwords were last changed.

The idea behind this is to lessen the probability that an attacker will infiltrate a system — or multiple systems if users reuse their passwords across different applications (this is more common than you think) — within an organization by acquiring old database backups containing users’ passwords.

Since I can’t show you what I built for that project, I cooked up another solution — a custom loggingin processor that determines whether a user’s password has expired in Sitecore:

using System;
using System.Web.Security;

using Sitecore.Diagnostics;
using Sitecore.Pipelines.LoggingIn;
using Sitecore.Web;

namespace Sitecore.Sandbox.Pipelines.LoggingIn
{
    public class CheckPasswordExpiration
    {
        private TimeSpan TimeSpanToExpirePassword { get; set; }
        private string ChangePasswordPageUrl { get; set; }

        public void Process(LoggingInArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!IsEnabled())
            {
                return;
            }

            MembershipUser user = GetMembershipUser(args);
            if (HasPasswordExpired(user))
            {
                WebUtil.Redirect(ChangePasswordPageUrl);
            }
        }

        private bool IsEnabled()
        {
            return IsTimeSpanToExpirePasswordSet() && IsChangePasswordPageUrlSet();
        }

        private bool IsTimeSpanToExpirePasswordSet()
        {
            return TimeSpanToExpirePassword > default(TimeSpan);
        }

        private bool IsChangePasswordPageUrlSet()
        {
            return !string.IsNullOrWhiteSpace(ChangePasswordPageUrl);
        }

        private static MembershipUser GetMembershipUser(LoggingInArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(args.Username, "args.Username");
            return Membership.GetUser(args.Username, false);
        }

        private bool HasPasswordExpired(MembershipUser user)
        {
            return user.LastPasswordChangedDate.Add(TimeSpanToExpirePassword) <= DateTime.Now;
        }
    }
}

The processor above ascertains whether a user’s password has expired by adding a configured timespan — see the configuration file below — to the last date and time the password was changed, and if that date and time summation is in the past — this means the password should have been changed already — then we redirect the user to a change password page (this is configured to be the Change Password page in Sitecore).

I wired up the custom loggingin processor, its timespan on expiring passwords — here I am using 1 minute since I can’t wait around all day ;) — and set the change password page to be the url of Sitecore’s Change Password page:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <loggingin>
        <processor mode="on" type="Sitecore.Sandbox.Pipelines.LoggingIn.CheckPasswordExpiration, Sitecore.Sandbox"
					patch:before="processor[@type='Sitecore.Pipelines.LoggingIn.CheckStartPage, Sitecore.Kernel']">
          <!-- Number of days, hours, minutes and seconds after the last password change date to expire passwords -->
          <TimeSpanToExpirePassword>00:00:01:00</TimeSpanToExpirePassword>
          <ChangePasswordPageUrl>/sitecore/login/changepassword.aspx</ChangePasswordPageUrl>
        </processor>  
      </loggingin>
    </processors>
  </sitecore>
</configuration>

Let’s test this out.

I went to Sitecore’s login page, and entered my username and password on the login form:

login-page

I clicked the Login button, and was redirected to the Change Password page as expected:

change-password-page

If you can think of any other security measures that should be added to Sitecore, please share in a comment.



Expand Your Scope: Add Additional Axes Via a Custom Sitecore Item Web API itemWebApiRequest Pipeline Processor

$
0
0

The Sitecore Item Web API offers client code the choice of retrieving an Item’s parent, the Item itself, all of its children, or any combination of these by simply setting the scope query string parameter in the request.

For example, if you want an Item’s children, you would only set the scope query string parameter to be the axe “c” — this would be scope=c — or if you wanted all data for the Item and its children, you would just set the scope query string parameter to be the self and children axes separated by a pipe — e.g. scope=s|c. Multiple axes must be separated by a pipe.

The other day, however, for my current project, I realized I needed a way to retrieve all data for an Item and all of its descendants via the Sitecore Item Web API.

The three options that ship with the Sitecore Item Web API cannot help me here, unless I want to make multiple requests to get data for an Item and all of it’s children, and then loop over all children and get their children, ad infinitum (well, hopefully it does stop somewhere).

Such a solution would require more development time — I would have to write additional code to do all of the looping — and this would — without a doubt — yield poorer performance versus getting all data upfront in a single request.

Through my excavating efforts in \App_Config\Include\Sitecore.ItemWebApi.config and Sitecore.ItemWebApi.dll, I discovered we can replace this “out of the box” functionality — this lives in /configuration/sitecore/pipelines/itemWebApiRequest/processor[@type="Sitecore.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.ItemWebApi"] in the Sitecore.ItemWebApi.config — via a custom itemWebApiRequest pipeline processor.

I thought it would be a good idea to define each of our scope operations in its own pipeline processor, albeit have all of these pipeline processors be nested within our itemWebApiRequest pipeline processor.

For the lack of a better term, I’m calling each of these a scope sub-pipeline processor (if you can think of a better name, or have seen this approach done before, please drop a comment).

The first thing I did was create a custom processor class to house two additional properties for our sub-pipeline processor:

using System.Xml;

using Sitecore.Pipelines;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request
{
    public class ScopeProcessor : Processor
    {
        public string Suppresses { get; private set; }
        public string QueryString { get; private set; }

        public ScopeProcessor(XmlNode configNode)
            : base(GetAttributeValue(configNode, "name"), GetAttributeValue(configNode, "type"), GetAttributeValue(configNode, "methodName"))
        {
            Suppresses = GetAttributeValue(configNode, "suppresses");
            QueryString = GetAttributeValue(configNode, "queryString");
        }
        public ScopeProcessor(string name, string type, string methodName, string suppresses, string queryString)
            : base(name, type, methodName)
        {
            Suppresses = suppresses;
            QueryString = queryString;
        }

        private static string GetAttributeValue(XmlNode configNode, string attributeName)
        {
            Assert.ArgumentNotNull(configNode, "configNode");
            Assert.ArgumentNotNullOrEmpty(attributeName, "attributeName");
            XmlAttribute attribute = configNode.Attributes[attributeName];

            if (attribute != null)
            {
                return attribute.Value;
            }

            return string.Empty;
        }
    }
}

The QueryString property will contain the axe for the given scope, and Suppresses property maps to another scope sub-pipeline processor query string value that will be ignored when both are present.

I then created a new PipelineArgs class for the scope sub-pipeline processors:

using System.Collections.Generic;

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

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request.DTO
{
    public class ScopeProcessorRequestArgs : PipelineArgs
    {
        private List<Item> _Items;
        public List<Item> Items
        {
            get
            {
                if (_Items == null)
                {
                    _Items = new List<Item>();
                }

                return _Items;
            }
            set
            {
                _Items = value;
            }
        }


        private List<Item> _Scope;
        public List<Item> Scope
        {
            get
            {
                if (_Scope == null)
                {
                    _Scope = new List<Item>();
                }

                return _Scope;
            }
            set
            {
                _Scope = value;
            }
        }

        public ScopeProcessorRequestArgs()
        {
        }
    }
}

Basically, the above class just holds Items that will be processed, and keeps track of Items in scope — these Items are added via the scope sub-pipeline processors for the supplied axes.

Now it’s time for our itemWebApiRequest pipeline processor:

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

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Request;
using Sitecore.Web;

using Sitecore.Sandbox.ItemWebApi.Pipelines.Request.DTO;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request
{
    public class ResolveScope : RequestProcessor
    {
        private IDictionary<string, ScopeProcessor> _ScopeProcessors;
        private IDictionary<string, ScopeProcessor> ScopeProcessors
        {
            get
            {
                if(_ScopeProcessors == null)
                {
                    _ScopeProcessors = new Dictionary<string, ScopeProcessor>();
                }

                return _ScopeProcessors;
            }
        }

        public override void Process(RequestArgs arguments)
        {
            if(!HasItemsInSet(arguments))
            {
                return;
            }

            arguments.Scope = GetItemsInScope(arguments).ToArray();
        }

        private static bool HasItemsInSet(RequestArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            Assert.ArgumentNotNull(arguments.Items, "arguments.Items");
            if (arguments.Items.Length < 1)
            {
                Logger.Warn("Cannot resolve the scope because the item set is empty.");
                arguments.Scope = new Item[0];
                return false;
            }

            return true;
        }

        private IEnumerable<Item> GetItemsInScope(RequestArgs arguments)
        {
            List<Item> itemsInScope = new List<Item>();
            foreach (Item item in arguments.Items)
            {
                ScopeProcessorRequestArgs args = new ScopeProcessorRequestArgs();
                args.Items.Add(item);
                GetItemsInScope(args);
                itemsInScope.AddRange(args.Scope);
            }

            return itemsInScope;
        }

        private void GetItemsInScope(ScopeProcessorRequestArgs arguments)
        {
            IEnumerable<ScopeProcessor> scopeProcessors = GetScopeProcessorsForRequest();
            foreach (ScopeProcessor scopeProcessor in scopeProcessors)
            {
                scopeProcessor.Invoke(arguments);
            }
        }

        private IEnumerable<ScopeProcessor> GetScopeProcessorsForRequest()
        {
            List<ScopeProcessor> scopeProcessors = GetScopeProcessorsForAxes();
            List<ScopeProcessor> scopeProcessorsForRequest = new List<ScopeProcessor>();
            foreach(ScopeProcessor scopeProcessor in scopeProcessors)
            {
                bool canAddProcessor = !scopeProcessors.Exists(processor => processor.Suppresses.Equals(scopeProcessor.QueryString));
                if (canAddProcessor)
                {
                    scopeProcessorsForRequest.Add(scopeProcessor);
                }
            }

            return scopeProcessorsForRequest;
        }

        private List<ScopeProcessor> GetScopeProcessorsForAxes()
        {
            List<ScopeProcessor> scopeProcessors = new List<ScopeProcessor>();
            foreach (string axe in GetAxes())
            {
                ScopeProcessor scopeProcessor;
                ScopeProcessors.TryGetValue(axe, out scopeProcessor);
                if(scopeProcessor != null && !scopeProcessors.Contains(scopeProcessor))
                {
                    scopeProcessors.Add(scopeProcessor);
                }
            }

            return scopeProcessors;
        }

        private IEnumerable<string> GetAxes()
        {
            string queryString = WebUtil.GetQueryString("scope", null);
            if (string.IsNullOrWhiteSpace(queryString))
            {
                return new string[] { "s" };
            }

            return queryString.Split(new char[] { '|' }).Distinct();
        }

        private IEnumerable<string> GetScopeProcessorQueryStringValues()
        {
            return ScopeProcessors.Values.Select(scopeProcessors => scopeProcessors.QueryString).ToList();
        }

        public virtual void AddScopeProcessor(XmlNode configNode)
        {
            ScopeProcessor scopeProcessor = new ScopeProcessor(configNode);
            bool canAdd = !string.IsNullOrEmpty(scopeProcessor.QueryString)
                            && !ScopeProcessors.ContainsKey(scopeProcessor.QueryString);

            if (canAdd)
            {
                ScopeProcessors.Add(scopeProcessor.QueryString, scopeProcessor);
            }
        }

        public virtual void AddItemSelf(ScopeProcessorRequestArgs arguments)
        {
            foreach (Item item in arguments.Items)
            {
                arguments.Scope.AddRange(GetCanBeReadItems(new Item[] { item }));
            }
        }

        public virtual void AddItemParent(ScopeProcessorRequestArgs arguments)
        {
            foreach (Item item in arguments.Items)
            {
                arguments.Scope.AddRange(GetCanBeReadItems(new Item[] { item.Parent }));
            }
        }
            
        public virtual void AddItemDescendants(ScopeProcessorRequestArgs arguments)
        {
            foreach (Item item in arguments.Items)
            {
                arguments.Scope.AddRange(GetCanBeReadItems(item.Axes.GetDescendants()));
            }
        }

        public virtual void AddItemChildren(ScopeProcessorRequestArgs arguments)
        {
            foreach(Item item in arguments.Items)
            {
                arguments.Scope.AddRange(GetCanBeReadItems(item.GetChildren()));
            }
        }

        private static IEnumerable<Item> GetCanBeReadItems(IEnumerable<Item> list)
        {
            if (list == null)
            {
                return new List<Item>();
            }

            return list.Where(item => CanReadItem(item));
        }

        private static bool CanReadItem(Item item)
        {
            return Context.Site.Name != "shell"
                    && item.Access.CanRead()
                    && item.Access.CanReadLanguage();
        }
    }
}

When this class is instantiated, each scope sub-pipeline processor is added to a dictionary, keyed by its query string axe value.

When this processor is invoked, it performs some validation — similarly to what is being done in the “out of the box” Sitecore.ItemWebApi.Pipelines.Request.ResolveScope class — and determines which scope processors are applicable for the given request. Only those that found in the dictionary via the supplied axes are used, minus those that are suppressed.

Once the collection of scope sub-pipeline processors is in place, each are invoked with a ScopeProcessorRequestArgs instance containing an Item to be processed.

When a scope sub-pipeline processor is done executing, Items that were retrieved from it are added into master list of scope Items to be returned to the caller.

I then glued all of this together — including the scope sub-pipeline processors — in \App_Config\Include\Sitecore.ItemWebApi.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- stuff is defined up here too -->
      <itemWebApiRequest>
		<!-- stuff is defined up here -->
		<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox">
		  <scopeProcessors hint="raw:AddScopeProcessor">
			<scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemSelf" name="self" queryString="s" />
			<scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemParent" name="parent" queryString="p" />
			<scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemDescendants" name="recursive" queryString="r" suppresses="c" />
			<scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemChildren" name="children" queryString="c" />
		  </scopeProcessors>
		</processor>
		<!-- some stuff is defined down here -->
		</itemWebApiRequest>
    </pipelines>
      <!-- there's more stuff defined down here -->
  </sitecore>
</configuration>

Let’s take the above for a spin.

First we need some items for testing. Lucky for me, I hadn’t cleaned up after myself when creating a previous blog post — yes, now I have a legitimate excuse for not picking up after myself — so let’s use these for testing:

items-in-sitecore-scope

After modifying some code in my copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK — I updated which item we are requesting conjoined for our scope query string parameter (scope=r) — I launched it to retrieve our test items:

scope-request-output

If you have any thoughts on this, or ideas on improving the above, please leave a comment.


Publish Items With the Sitecore Item Web API Using a Custom ResolveAction itemWebApiRequest Pipeline Processor

$
0
0

At the end of last week, when many people were probably thinking about what to do over the weekend, or were making plans with family and/or friends, I started thinking about what I might need to do next on the project I’m working on.

I realized I might need a way to publish Sitecore items I’ve touched via the Sitecore Item Web API — a feature that appears to be missing, or I just cannot figure out how to use from its documentation (if there is a way to do this “out of the box”, please share in a comment below).

After some digging around in Sitecore.ItemWebApi.dll and \App_Config\Include\Sitecore.ItemWebApi.config, I thought it would be a good idea to define a new action that employs a request method other than get, post, put, delete — these request methods are used by a vanilla install of the Sitecore Item Web API.

Where would one find a list of “standard” request methods? Of course Google knows all — I learned about the patch request method, and decided to use it.

According to Wikipedia — see this entry subsection discussing request methods — the patch request method is “used to apply partial modifications to a resource”, and one could argue that a publishing an item in Sitecore is a partial modification to that item — it’s being pushed to another database which is really an update on that item in the target database.

Now that our research is behind us — and we’ve learned about the patch request method — let’s get our hands dirty with some code.

Following the pipeline processor convention set forth in code for the Sitecore Item Web API for other request methods, I decide to box our new patch requests into a new pipeline, and doing this called for creating a new data transfer object for the new pipeline processor we will define below:

using Sitecore.Data.Items;

using Sitecore.ItemWebApi.Pipelines;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Patch.DTO
{
    public class PatchArgs : OperationArgs
    {
        public PatchArgs(Item[] scope)
            : base(scope)
        {
        }
    }
}

Next, I created a base class for our new patch processor:

using Sitecore.ItemWebApi.Pipelines;

using Sitecore.Sandbox.ItemWebApi.Pipelines.Patch.DTO;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Patch
{
    public abstract class PatchProcessor : OperationProcessor<PatchArgs>
    {
        protected PatchProcessor()
        {
        }
    }
}

I then created a new pipeline processor that will publish items passed to it:

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

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.Publishing;
using Sitecore.Web;

using Sitecore.Sandbox.ItemWebApi.Pipelines.Patch.DTO;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Patch
{
    public class PublishScope : PatchProcessor
    {
        private string DefaultTargetDatabase { get; set; }

        public override void Process(PatchArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            Assert.IsNotNull(arguments.Scope, "The scope is null!");
            PublishItems(arguments.Scope, GetTargetDatabase());
            arguments.Result = GetResult(arguments.Scope);
        }

        private Database GetTargetDatabase()
        {
            return Factory.GetDatabase(GetTargetDatabaseName());
        }

        private string GetTargetDatabaseName()
        {
            string databaseName = WebUtil.GetQueryString("sc_target_database");
            if(!string.IsNullOrWhiteSpace(databaseName))
            {
                return databaseName;
            }

            Assert.IsNotNullOrEmpty(DefaultTargetDatabase, "DefaultTargetDatabase must be set!");
            return DefaultTargetDatabase;
        }

        private static void PublishItems(IEnumerable<Item> items, Database database)
        {
            foreach(Item item in items)
            {
                PublishItem(item, database);
            }
        }

        private static void PublishItem(Item item, Database database)
        {
           PublishOptions publishOptions = new PublishOptions
           (
               item.Database,
               database,
               Sitecore.Publishing.PublishMode.SingleItem,
               item.Language,
               DateTime.Now
           );

            Publisher publisher = new Publisher(publishOptions);
            publisher.Options.RootItem = item;
            publisher.Publish();
        }

        private static Dynamic GetResult(IEnumerable<Item> scope)
        {
            Assert.ArgumentNotNull(scope, "scope");
            Dynamic dynamic = new Dynamic();
            dynamic["statusCode"] = 200;
            dynamic["result"] = GetInnerResult(scope);
            return dynamic;
        }

        private static Dynamic GetInnerResult(IEnumerable<Item> scope)
        {
            Assert.ArgumentNotNull(scope, "scope");
            Dynamic dynamic = new Dynamic();
            dynamic["count"] = scope.Count();
            dynamic["itemIds"] = scope.Select(item => item.ID.ToString());
            return dynamic;
        }
    }
}

The above pipeline processor class serially publishes each item passed to it, and returns a Sitecore.ItemWebApi.Dynamic instance containing information on how many items were published; a collection of IDs of the items that were published; and an OK status code.

If the calling code does not supply a publishing target database via the sc_target_database query string parameter, the processor will use the value defined in DefaultTargetDatabase — this value is set in \App_Config\Include\Sitecore.ItemWebApi.config, which you will see later when I show changes I made to this file.

It had been awhile since I’ve had to publish items in code, so I searched for a refresher on how to do this.

In my quest for some Sitecore API code, I rediscovered this article by Sitecore MVP
Brian Pedersen showing how one can publish Sitecore items programmatically — many thanks to Brian for this article!

If you haven’t read Brian’s article, you should go check it out now. Don’t worry, I’ll wait. :)

I then created a new ResolveAction itemWebApiRequest pipeline processor:

using System;

using Sitecore.Diagnostics;
using Sitecore.Exceptions;
using Sitecore.ItemWebApi.Pipelines.Request;
using Sitecore.ItemWebApi.Security;
using Sitecore.Pipelines;

using Sitecore.Sandbox.ItemWebApi.Pipelines.Patch.DTO;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request
{
    public class ResolveAction : Sitecore.ItemWebApi.Pipelines.Request.ResolveAction
    {
        public override void Process(RequestArgs requestArgs)
        {
            Assert.ArgumentNotNull(requestArgs, "requestArgs");
            string method = GetMethod(requestArgs.Context);
            AssertOperation(requestArgs, method);
            
            if(IsCreateRequest(method))
            {
	            ExecuteCreateRequest(requestArgs);
	            return;
            }

            if(IsReadRequest(method))
            {
	            ExecuteReadRequest(requestArgs);
	            return;
            }

            if(IsUpdateRequest(method))
            {
               ExecuteUpdateRequest(requestArgs);
               return;
            }

            if(IsDeleteRequest(method))
            {
	            ExecuteDeleteRequest(requestArgs);
	            return;
            }

            if (IsPatchRequest(method))
            {
	            ExecutePatchRequest(requestArgs);
	            return;
            }
        }

        private static void AssertOperation(RequestArgs requestArgs, string method)
        {
            Assert.ArgumentNotNull(requestArgs, "requestArgs");
            if (requestArgs.Context.Settings.Access == AccessType.ReadOnly && !AreEqual(method, "get"))
            {
                throw new AccessDeniedException("The operation is not allowed.");
            }
        }

        private static bool IsCreateRequest(string method)
        {
            return AreEqual(method, "post");
        }

        private static bool IsReadRequest(string method)
        {
            return AreEqual(method, "get");
        }

        private static bool IsUpdateRequest(string method)
        {
            return AreEqual(method, "put");
        }

        private static bool IsDeleteRequest(string method)
        {
            return AreEqual(method, "delete");
        }

        private static bool IsPatchRequest(string method)
        {
            return AreEqual(method, "patch");
        }

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

        protected virtual void ExecutePatchRequest(RequestArgs requestArgs)
        {
            Assert.ArgumentNotNull(requestArgs, "requestArgs");
            PatchArgs patchArgsArgs = new PatchArgs(requestArgs.Scope);
            CorePipeline.Run("itemWebApiPatch", patchArgsArgs);
            requestArgs.Result = patchArgsArgs.Result;
        }

        private string GetMethod(Sitecore.ItemWebApi.Context context)
        {
            Assert.ArgumentNotNull(context, "context");
            return context.HttpContext.Request.HttpMethod.ToLower();
        }
    }
}

The class above contains the same logic as Sitecore.ItemWebApi.Pipelines.Request.ResolveAction, though I refactored it a bit — the nested conditional statements in the Process method were driving me bonkers, and I atomized logic by placing into new methods.

Plus, I added an additional check to see if the request we are to execute is a patch request — this is true when HttpContext.Request.HttpMethod.ToLower() in our Sitecore.ItemWebApi.Context instance is equal to “patch” — and call our new patch pipeline if this is the case.

I then added the new itemWebApiPatch pipeline with its new PublishScope processor, and replaced /configuration/sitecore/pipelines/itemWebApiRequest/processor[@type="Sitecore.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.ItemWebApi"] with /configuration/sitecore/pipelines/itemWebApiRequest/processor[@type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.Sandbox"] in \App_Config\Include\Sitecore.ItemWebApi.config:

<?xml version="1.0" encoding="utf-8"?>
	<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
		<sitecore>
			<pipelines>
				<itemWebApiRequest>
					<!-- stuff is defined up here -->
					<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.Sandbox" />
					<!-- stuff is defined right here -->
				</itemWebApiRequest>
				<!-- more stuff is defined here -->
				<itemWebApiPatch>
					<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Patch.PublishScope, Sitecore.Sandbox">
						<DefaultTargetDatabase>web</DefaultTargetDatabase>
					</processor>
				</itemWebApiPatch>
			</pipelines>
			<!-- there's even more stuff defined down here -->
		</sitecore>
	</configuration>

Let’s test this out, and see how we did.

We’ll be publishing these items:

master-items-to-publish

As you can see, they aren’t in the web database right now:

arent-there-yet-web

I had to modify code in my copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK, to use the patch request method for the ancestor home item shown above, and set a scope of self and recursive (scope=s|r) — checkout out this post where I added the recursive axe to the Sitecore Item Web API. I am excluding the console application code modification for the sake of brevity.

I then ran the console application above, and saw this:

after-publishing-output

All of these items now live in the web database:

all-in-web-after-publish

If you have any thoughts on this, or ideas on other useful actions for the Sitecore Item Web API, please drop a comment.


Set New Media Library Item Fields Via the Sitecore Item Web API

$
0
0

On a recent project, I found the need to set field data on new media library items using the Sitecore Item Web API — a feature that is not supported “out of the box”.

After digging through Sitecore.ItemWebApi.dll, I discovered where one could add the ability to update fields on newly created media library items:

CreateMediaItems-out-of-box

Unfortunately, the CreateMediaItems method in the Sitecore.ItemWebApi.Pipelines.Request.ResolveAction class is declared private — introducing code to set fields on new media library items will require some copying and pasting of code.

Honestly, I loathe duplicating code. :(

Unfortunately, we must do it in order to add the capability of setting fields on media library items via the Sitecore Item Web API (if you can think of a better way, please leave a comment).

I did just that on the following subclass of Sitecore.ItemWebApi.Pipelines.Request.ResolveAction:

using System;
using System.Collections.Generic;
using System.IO;
using System.Web;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.IO;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Read;
using Sitecore.ItemWebApi.Pipelines.Request;
using Sitecore.Pipelines;
using Sitecore.Resources.Media;
using Sitecore.Text;
using Sitecore.Data.Fields;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request
{
    public class ResolveActionMediaItems : ResolveAction
    {
        protected override void ExecuteCreateRequest(RequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (IsMediaCreation(args.Context))
            {
                CreateMediaItems(args);
                return;
            }

            base.ExecuteCreateRequest(args);
        }

        private void CreateMediaItems(RequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Item parent = args.Context.Item;
            if (parent == null)
            {
                throw new Exception("The specified location not found.");
            }
            string fullPath = parent.Paths.FullPath;
            if (!fullPath.StartsWith("/sitecore/media library"))
            {
                throw new Exception(string.Format("The specified location of media items is not in the Media Library ({0}).", fullPath));
            }
            string name = args.Context.HttpContext.Request.Params["name"];
            if (string.IsNullOrEmpty(name))
            {
                throw new Exception("Item name not specified (HTTP parameter 'name').");
            }
            Database database = args.Context.Database;
            Assert.IsNotNull(database, "Database not resolved.");
            HttpFileCollection files = args.Context.HttpContext.Request.Files;
            Assert.IsTrue(files.Count > 0, "Files not found.");
            List<Item> list = new List<Item>();

            for (int i = 0; i < files.Count; i++)
            {
                HttpPostedFile file = files[i];
                if (file.ContentLength != 0)
                {
                    string fileName = file.FileName;
                    string uniqueName = ItemUtil.GetUniqueName(parent, name);
                    string destination = string.Format("{0}/{1}", fullPath, uniqueName);
                    MediaCreatorOptions options = new MediaCreatorOptions
                    {
                        AlternateText = fileName,
                        Database = database,
                        Destination = destination,
                        Versioned = false
                    };
                    Stream inputStream = file.InputStream;
                    string extension = FileUtil.GetExtension(fileName);
                    string filePath = string.Format("{0}.{1}", uniqueName, extension);
                    try
                    {
                        Item item = MediaManager.Creator.CreateFromStream(inputStream, filePath, options);
                        SetFields(item, args.Context.HttpContext.Request["fields"]); // MR: set field data on item if data is passed
                        list.Add(item);
                    }
                    catch
                    {
                        Logger.Warn("Cannot create the media item.");
                    }
                }
            }
            ReadArgs readArgs = new ReadArgs(list.ToArray());
            CorePipeline.Run("itemWebApiRead", readArgs);
            args.Result = readArgs.Result;
        }

        private static void SetFields(Item item, string fieldsQueryString)
        {
            if (!string.IsNullOrWhiteSpace(fieldsQueryString))
            {
                SetFields(item, new UrlString(fieldsQueryString));
            }
        }

        private static void SetFields(Item item, UrlString fields)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNull(fields, "fields");

            if (fields.Parameters.Count < 1)
            {
                return;
            }

            item.Editing.BeginEdit();

            foreach (string fieldName in fields.Parameters.Keys)
            {
                Field field = item.Fields[fieldName];
                if(field != null)
                {
                    field.Value = fields.Parameters[fieldName];
                }
            }

            item.Editing.EndEdit();
        }

        private bool IsMediaCreation(Sitecore.ItemWebApi.Context context)
        {
            Assert.ArgumentNotNull(context, "context");
            return context.HttpContext.Request.Files.Count > 0;
        }
    }
}

The above class reads fields supplied by client code via a query string passed in a query string parameter — the fields query string must be “url encoded” by the client code before being passed in the outer query string.

We then delegate to an instance of the Sitecore.Text.UrlString class when fields are supplied by client code — if you don’t check to see if the query string is null, empty or whitespace, the UrlString class will throw an exception if it’s not set — to parse the fields query string, and loop over the parameters within it — each parameter represents a field to be set on the item, and is set if it exists (see the SetFields methods above).

I replaced Sitecore.ItemWebApi.Pipelines.Request.ResolveAction in \App_Config\Include\Sitecore.ItemWebApi.config with our new pipeline processor above:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<pipelines>
		<!-- there is more stuff up here -->
		<!--Processes Item Web API requests. -->
		<itemWebApiRequest>
			<!-- there are more pipeline processors up here -->
			<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveActionMediaItems, Sitecore.Sandbox" />
			<!-- there are more pipeline processors down here -->
		</itemWebApiRequest>
		<!-- there is more stuff down here -->
		</pipelines>
	</sitecore>
</configuration>

I then modified the media library item creation code in my copy of the console application written by Kern Herskind Nightingale — Director of Technical Services at Sitecore UK — to send field data to the Sitecore Item Web API:

create-media-console-2

I set some fields using test data:

create-media-console-1

After I ran the console application, I got a response — this is a good sign :)

pizza-created-result

As you can see, our test field data has been set on our pizza media library item:

pizza-in-media-library

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

Now I’m hungry — perhaps I’ll order a pizza! :)


Content Manage Links to File System Favicons for Multiple Sites Managed in Sitecore

$
0
0

Earlier today someone started a thread in one of the SDN forums asking how to go about adding the ability to have a different favicon for each website managed in the same instance of Sitecore.

I had implemented this in the past for a few clients, and thought I should write a post on how I had done this.

In most of those solutions, the site’s start item would contain a “server file” field — yes I know it’s deprecated but it works well for this (if you can suggested a better field type to use, please leave a comment below) — that would point to a favicon on the file system:

server-file-favicon

Content authors/editors can then choose the appropriate favicon for each site managed in their Sitecore instance — just like this:

linked-to-smiley-favicon

Not long after the SDN thread was started, John West — Chief Technology Officer at Sitecore USA — wrote a quick code snippet, followed by a blog post on how one might go about doing this.

John’s solution is a different than the one I had used in the past — each site’s favicon is defined on its site node in the Web.config.

After seeing John’s solution, I decided I would create a hybrid solution — the favicon set on the start item would have precedence over the one defined on the site node in the Web.config. In other words, the favicon defined on the site node would be a fallback.

For this hybrid solution, I decided to create a custom pipeline to retrieve the favicon for the context site, and created the following pipeline arguments class for it:

using System.Web.UI;

using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.GetFavicon
{
    public class FaviconTryGetterArgs : PipelineArgs
    {
        public string FaviconUrl { get; set; }

        public Control FaviconControl{ get; set; }
    }
}

The idea is to have pipeline processors set the URL of the favicon if possible, and have another processor create an ASP.NET control for the favicon when the URL is supplied.

The following class embodies this high-level idea:

using System;

using System.Web.UI;
using System.Web.UI.HtmlControls;

using Sitecore;
using Sitecore.Configuration;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Sandbox.Utilities.Extensions;

namespace Sitecore.Sandbox.Pipelines.GetFavicon
{
    public class FaviconTryGetter
    {
        private string FaviconFieldName { get; set; }

        public void TryGetFromStartItem(FaviconTryGetterArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            bool canProcess = !string.IsNullOrWhiteSpace(FaviconFieldName) 
                                && Context.Site != null 
                                && !string.IsNullOrWhiteSpace(Context.Site.StartPath);

            if (!canProcess)
            {
                return;
            }

            Item startItem = Context.Database.GetItem(Context.Site.StartPath);
            args.FaviconUrl = startItem[FaviconFieldName];
        }

        public void TryGetFromSite(FaviconTryGetterArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            bool canProcess = Context.Site != null 
                                && string.IsNullOrWhiteSpace(args.FaviconUrl);

            if (!canProcess)
            {
                return;
            }
            
			/* GetFavicon is an extension method borrowed from John West. You can find it at  http://www.sitecore.net/Community/Technical-Blogs/John-West-Sitecore-Blog/Posts/2013/08/Use-Different-Shortcut-Icons-for-Different-Managed-Sites-with-the-Sitecore-ASPNET-CMS.aspx 
            */
            args.FaviconUrl = Context.Site.GetFavicon(); 
        }

        public void TryGetFaviconControl(FaviconTryGetterArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(string.IsNullOrWhiteSpace(args.FaviconUrl))
            {
                return; 
            }
            
            args.FaviconControl = CreateNewFaviconControl(args.FaviconUrl);
        }

        private static Control CreateNewFaviconControl(string faviconUrl)
        {
            Assert.ArgumentNotNullOrEmpty(faviconUrl, "faviconUrl");
            HtmlLink link = new HtmlLink();
            link.Attributes.Add("type", "image/x-icon");
            link.Attributes.Add("rel", "icon");
            link.Href = faviconUrl;
            return link;
        }
    }
}

The TryGetFromStartItem method tries to get the favicon set on the favicon field on the start item — the name of the field is supplied via one of the processors defined in the configuration include file below — and sets it on the FaviconUrl property of the FaviconTryGetterArgs instance supplied by the caller.

If the field name for the field containing the favicon is not supplied, or there is something wrong with either the context site or the start item’s path, then the method does not finish executing.

The TryGetFromSite method is similar to what John had done in his post. It uses the same exact extension method John had used for getting the favicon off of a “favicon” attribute set on the context site’s node in the Web.config — I have omitted this extension method and its class since you can check it out in John’s post.

If a URL is set by either of the two methods discussed above, the TryGetFaviconControl method creates an instance of an HtmlLink System.Web.UI.HtmlControls.HtmlControl, sets the appropriate attributes for an html favicon link tag, and sets it in the FaviconControl property of the FaviconTryGetterArgs instance.

I assembled the methods above into a new getFavicon pipeline in the following configuration include file, and also set a fallback favicon for my local sandbox site’s configuration element:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getFavicon>
        <processor type="Sitecore.Sandbox.Pipelines.GetFavicon.FaviconTryGetter, Sitecore.Sandbox" method="TryGetFromStartItem">
          <FaviconFieldName>Favicon</FaviconFieldName>
        </processor> 
        <processor type="Sitecore.Sandbox.Pipelines.GetFavicon.FaviconTryGetter, Sitecore.Sandbox" method="TryGetFromSite" />
        <processor type="Sitecore.Sandbox.Pipelines.GetFavicon.FaviconTryGetter, Sitecore.Sandbox" method="TryGetFaviconControl" />
      </getFavicon>
    </pipelines>
    <sites>
      <site name="website">
        <patch:attribute name="favicon">/sitecore.ico</patch:attribute>
      </site>
    </sites>
  </sitecore>
</configuration>

Just as John West had done in his post, I created a custom WebControl for rendering the favicon, albeit the following class invokes our new pipeline above to get the favicon ASP.NET control:

using System.Web.UI;

using Sitecore.Pipelines;
using Sitecore.Sandbox.Pipelines.GetFavicon;
using Sitecore.Web.UI;

namespace Sitecore.Sandbox.WebControls
{
    public class Favicon : WebControl 
    {
        protected override void DoRender(HtmlTextWriter output)
        {
            FaviconTryGetterArgs args = new FaviconTryGetterArgs();
            CorePipeline.Run("getFavicon", args);
            if (args.FaviconControl != null)
            {
                args.FaviconControl.RenderControl(output);
            }
        }
    }
}

If a favicon Control is supplied by our new getFavicon pipeline, the WebControl then delegates rendering responsibility to it.

I then defined an instance of the WebControl above in my default layout:

<%@ Register TagPrefix="sj" Namespace="Sitecore.Sharedsource.Web.UI.WebControls" Assembly="Sitecore.Sharedsource" %>
...
<html>
  <head>
  ...
  <sj:Favicon runat="server" /> 
  ...

For testing, I found a favicon generator website out on the internet — I won’t share this since it’s appeared to be a little suspect — and created a smiley face favicon. I set this on my start item, and published:

smiley-favicon

After clearing it out on my start item, and publishing, the fallback Sitecore favicon appears:

sitecore-favicon

When you remove all favicons, none appear.

no-favicon

If you have any thoughts, suggestions, or comments on this, please share below.


Navigate to Base Templates of a Template using a Sitecore Command

$
0
0

Have you ever said to yourself when looking at base templates of a template in its Content tab “wouldn’t it be great if I could easily navigate to one of these?”

the-problem-1

I have had this thought more than once despite having the ability to do this in a template’s Inheritance tab — you can do this by clicking one of the base template links listed:

inheritance-tab

For some reason I sometimes forget you have the ability to get to a base template of a template in the Inheritance tab — why I forget is no doubt a larger issue I should try to tackle, albeit I’ll leave that for another day — and decided to build something that will be more difficult for me to forget: launching a dialog via a new item context menu option, and selecting one of the base templates of a template in that dialog.

I decided to atomize functionality in my solution by building custom pipelines/processors wherever I felt doing so made sense.

I started off by building a custom pipeline that gets base templates for a template, and defined a data transfer object (DTO) class for it:

using System.Collections.Generic;

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

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GetBaseTemplatesArgs : PipelineArgs
    {
        public TemplateItem TemplateItem { get; set; }

        public bool IncludeAncestorBaseTemplates { get; set; }

        private List<TemplateItem> _BaseTemplates;
        public List<TemplateItem> BaseTemplates 
        {
            get
            {
                if (_BaseTemplates == null)
                {
                    _BaseTemplates = new List<TemplateItem>();
                }

                return _BaseTemplates;
            }
            set
            {
                _BaseTemplates = value;
            }
        }
    }
}

Client code must supply the template item that will be used as the starting point for gathering base templates, and can request all ancestor base templates — excluding the Standard Template as you will see below — by setting the IncludeAncestorBaseTemplates property to true.

I then created a class with a Process method that will serve as the only pipeline processor for my new pipeline:

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

using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GetBaseTemplates
    {
        public void Process(GetBaseTemplatesArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.TemplateItem, "args.TemplateItem");
            List<TemplateItem> baseTemplates = new List<TemplateItem>();
            GatherBaseTemplateItems(baseTemplates, args.TemplateItem, args.IncludeAncestorBaseTemplates);
            args.BaseTemplates = baseTemplates;
        }

        private static void GatherBaseTemplateItems(List<TemplateItem> baseTemplates, TemplateItem templateItem, bool includeAncestors)
        {
            if (includeAncestors)
            {
                foreach (TemplateItem baseTemplateItem in templateItem.BaseTemplates)
                {
                    GatherBaseTemplateItems(baseTemplates, baseTemplateItem, includeAncestors);
                }
            }

            if (!IsStandardTemplate(templateItem) && templateItem.BaseTemplates != null && templateItem.BaseTemplates.Any())
            {
                baseTemplates.AddRange(GetBaseTemplatesExcludeStandardTemplate(templateItem.BaseTemplates));
            }
        }

        private static IEnumerable<TemplateItem> GetBaseTemplatesExcludeStandardTemplate(TemplateItem templateItem)
        {
            if (templateItem == null)
            {
                return new List<TemplateItem>();
            }

            return GetBaseTemplatesExcludeStandardTemplate(templateItem.BaseTemplates);
        }

        private static IEnumerable<TemplateItem> GetBaseTemplatesExcludeStandardTemplate(IEnumerable<TemplateItem> baseTemplates)
        {
            if (baseTemplates != null && baseTemplates.Any())
            {
                return baseTemplates.Where(baseTemplate => !IsStandardTemplate(baseTemplate));
            }

            return baseTemplates;
        }

        private static bool IsStandardTemplate(TemplateItem templateItem)
        {
            return templateItem.ID == TemplateIDs.StandardTemplate;
        }
    }
}

Methods in the above class add base templates to a list when the templates are not the Standard Template — I thought it would be a rare occurrence for one to navigate to it, and decided not to include it in the collection.

Further, the method that gathers base templates is recursively executed when client code requests all ancestor base templates be include in the collection.

The next thing I built was functionality to prompt the user for a base template via a dialog, and track which base template was chosen. I decided to do this using a custom client processor, and built the following DTO for it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Web.UI.Sheer;
using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GotoBaseTemplateArgs : ClientPipelineArgs
    {
        public TemplateItem TemplateItem { get; set; }

        public string SelectedBaseTemplateId { get; set; }
    }
}

Just like the other DTO defined above, client code must suppy a template item. The SelectedBaseTemplateId property is set after a user selects a base template in the modal launched by the following class:

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

using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Applications.Dialogs.ItemLister;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GotoBaseTemplate
    {
        public string SelectTemplateButtonText { get; set; }

        public string ModalIcon { get; set; }

        public string ModalTitle { get; set; }

        public string ModalInstructions { get; set; }

        public void SelectBaseTemplate(GotoBaseTemplateArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.TemplateItem, "args.TemplateItem");
            Assert.ArgumentNotNullOrEmpty(SelectTemplateButtonText, "SelectTemplateButtonText");
            Assert.ArgumentNotNullOrEmpty(ModalIcon, "ModalIcon");
            Assert.ArgumentNotNullOrEmpty(ModalTitle, "ModalTitle");
            Assert.ArgumentNotNullOrEmpty(ModalInstructions, "ModalInstructions");
            
            if (!args.IsPostBack)
            {
                ItemListerOptions itemListerOptions = new ItemListerOptions
                {
                    ButtonText = SelectTemplateButtonText,
                    Icon = ModalIcon,
                    Title = ModalTitle,
                    Text = ModalInstructions
                };

                itemListerOptions.Items = GetBaseTemplateItemsForSelection(args.TemplateItem).Select(template => template.InnerItem).ToList();
                itemListerOptions.AddTemplate(TemplateIDs.Template);
                SheerResponse.ShowModalDialog(itemListerOptions.ToUrlString().ToString(), true);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.SelectedBaseTemplateId = args.Result;
                args.IsPostBack = false;
            }
            else
            {
                args.AbortPipeline();
            }
        }

        private IEnumerable<TemplateItem> GetBaseTemplateItemsForSelection(TemplateItem templateItem)
        {
            GetBaseTemplatesArgs args = new GetBaseTemplatesArgs
            {
                TemplateItem = templateItem,
                IncludeAncestorBaseTemplates = true,
            };
            CorePipeline.Run("getBaseTemplates", args);
            return args.BaseTemplates;
        }

        public void Execute(GotoBaseTemplateArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(args.SelectedBaseTemplateId, "args.SelectedBaseTemplateId");
            Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", args.SelectedBaseTemplateId), 1);
        }
    }
}

The SelectBaseTemplate method above gives the user a list of base templates to choose from — this includes all ancestor base templates of a template minus the Standard Template.

The title, icon, helper text of the modal are supplied via the processor’s xml node in its configuration file — you’ll see this later on in this post.

Once a base template is chosen, its Id is then set in the SelectedBaseTemplateId property of the GotoBaseTemplateArgs instance.

The Execute method brings the user to the selected base template item in the Sitecore content tree.

Now we need a way to launch the code above.

I did this using a custom command that will be wired up to the item context menu:

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

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

using Sitecore.Sandbox.Shell.Framework.Pipelines;
using Sitecore.Web.UI.Sheer;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Commands
{
    public class GotoBaseTemplateCommand : Command
    {
        public override void Execute(CommandContext context)
        {
            Context.ClientPage.Start("gotoBaseTemplate", new GotoBaseTemplateArgs { TemplateItem = GetItem(context) });
        }

        public override CommandState QueryState(CommandContext context)
        {
            if (ShouldEnable(GetItem(context)))
            {
                return CommandState.Enabled;
            }

            return CommandState.Hidden;
        }

        private static bool ShouldEnable(Item item)
        {
            return item != null
                    && IsTemplate(item)
                    && GetBaseTemplates(item).Any();
        }

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

        private static bool IsTemplate(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return TemplateManager.IsTemplate(item);
        }

        private static IEnumerable<TemplateItem> GetBaseTemplates(TemplateItem templateItem)
        {
            Assert.ArgumentNotNull(templateItem, "templateItem");
            GetBaseTemplatesArgs args = new GetBaseTemplatesArgs 
            { 
                TemplateItem = templateItem, 
                IncludeAncestorBaseTemplates = false 
            };

            CorePipeline.Run("getBaseTemplates", args);
            return args.BaseTemplates;
        }
    }
}

The command above is visible only when the item is a template, and has base templates on it — we invoke the custom pipeline built above to get base templates.

When the command is invoked, we call our custom client processor to prompt the user for a base template to go to.

I then glued everything together using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <sitecore>
    <commands>
      <command name="item:GotoBaseTemplate" type="Sitecore.Sandbox.Commands.GotoBaseTemplateCommand, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <getBaseTemplates>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.GetBaseTemplates, Sitecore.Sandbox"/>
      </getBaseTemplates>
    </pipelines>
    <processors>
      <gotoBaseTemplate>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.GotoBaseTemplate, Sitecore.Sandbox" method="SelectBaseTemplate">
          <SelectTemplateButtonText>OK</SelectTemplateButtonText>
          <ModalIcon>Applications/32x32/nav_up_right_blue.png</ModalIcon>
          <ModalTitle>Select A Base Template</ModalTitle>
          <ModalInstructions>Select the base template you want to navigate to.</ModalInstructions>
        </processor>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.GotoBaseTemplate, Sitecore.Sandbox" method="Execute"/>
      </gotoBaseTemplate>
    </processors>
  </sitecore>
</configuration>

I’ve left out how I’ve added the command shown above to the item context menu in the core database. For more information on adding to the item context menu, please see part one and part two of my post showing how to do this.

Let’s see how we did.

I first created some templates for testing. The following template named ‘Meta’ uses two other test templates as base templates:

meta-template

I also created a ‘Base Page’ template which uses the ‘Meta’ template above:

base-page-template

Next I created ‘The Coolest Page Template Ever’ template — this uses the ‘Base Page’ template as its base template:

the-coolest-page-template-ever-template

I then right-clicked on ‘The Coolest Page Template Ever’ template to launch its context menu, and selected our new menu option:

context-menu-go-to-base-template

I was then presented with a dialog asking me to select the base template I want to navigate to:

base-template-lister-modal-1

I chose one of the base templates, and clicked ‘OK’:

base-template-lister-modal-2

I was then brought to the base template I had chosen:

brought-to-selected-base-template

If you have any thoughts on this, please leave a comment.


Shortcodes in Sitecore: A Proof of Concept

$
0
0

Today I stumbled upon a post in one of the SDN forums asking whether anyone had ever implemented shortcodes in Sitecore.

I have not seen an implementation of this for Sitecore — if you know of one, please drop a comment — but am quite familiar with these in WordPress — I use them to format code in my blog posts using the [code language=”csharp”]//code goes in here[/code] shortcode — and felt I should take on the challenge of implementing a “proof of concept” for this in Sitecore.

I first created a POCO that will hold shortcode data: the shortcode itself and the content (or markup) that the shortcode represents after being expanded:

namespace Sitecore.Sandbox.Pipelines.ExpandShortcodes
{
    public class Shortcode
    {
        public string Unexpanded { get; set; }

        public string Expanded { get; set; }
    }
}

I thought it would be best to put the logic that expands shortcodes into a new pipeline, and defined a pipeline arguments class for it:

using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.ExpandShortcodes
{
    public class ExpandShortcodesArgs : PipelineArgs
    {
        public string Content { get; set; }
    }
}

There really isn’t much to this arguments class — we will only be passing around a string of content that will contain shortcodes to be expanded.

Before moving forward on building pipeline processors for the new pipeline, I saw that I could leverage the template method pattern to help me process collections of Shortcode instances in an abstract base class:

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandShortcodes
{
    public abstract class ExpandShortcodesProcessor
    {
        public virtual void Process(ExpandShortcodesArgs args)
        {
            if (string.IsNullOrWhiteSpace(args.Content))
            {
                return;
            }

            IEnumerable<Shortcode> shortcodes = GetShortcodes(args.Content);
            if (shortcodes == null || !shortcodes.Any())
            {
                return;
            }

            args.Content = ExpandShortcodes(shortcodes, args.Content);
        }

        protected abstract IEnumerable<Shortcode> GetShortcodes(string content);

        protected virtual string ExpandShortcodes(IEnumerable<Shortcode> shortcodes, string content)
        {
            Assert.ArgumentNotNull(shortcodes, "shortcodes");
            Assert.ArgumentNotNull(content, "content");
            string contentExpanded = content;
            foreach (Shortcode shortcode in shortcodes)
            {
                contentExpanded = contentExpanded.Replace(shortcode.Unexpanded, shortcode.Expanded);
            }

            return contentExpanded;
        }
    }
}

The above class iterates over all Shortcode instances, and replaces shortcodes with their expanded content.

Each subclass processor of ExpandShortcodesProcessor are to “fill in the blanks” of the algorithm defined in the base class by implementing the GetShortcodes method only — this is where the heavy lifting of grabbing the shortcodes from the passed string of content, and the expansion of these shortcodes are done. Both are then set in new Shortcode instances.

Once the base class was built, I developed an example ExpandShortcodesProcessor subclass to expand [BigBlueText]content goes in here[/BigBlueText] shortcodes (in case you’re wondering, I completely fabricated this shortcode — it does not exist in the real world):

using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace Sitecore.Sandbox.Pipelines.ExpandShortcodes
{
    public class ExpandBigBlueTextShortcodes : ExpandShortcodesProcessor
    {
        protected override IEnumerable<Shortcode> GetShortcodes(string content)
        {
            if(string.IsNullOrWhiteSpace(content))
            {
                return new List<Shortcode>();
            }

            IList<Shortcode> shortcodes = new List<Shortcode>();
            MatchCollection matches = Regex.Matches(content, @"\[BigBlueText\](.*?)\[/BigBlueText\]", RegexOptions.IgnoreCase);

            foreach (Match match in matches)
            {
                string innerText = match.Groups[1].Value.Trim();
                if (!string.IsNullOrWhiteSpace(innerText))
                {
                    shortcodes.Add
                    (
                        new Shortcode
                        {
                            Unexpanded = match.Value,
                            Expanded = string.Format(@"<span style=""font-size:56px;color:blue;"">{0}</span>", innerText)
                        }
                    );
                }
            }

            return shortcodes;
        }
    }
}

I followed the above example processor with another — a new one to expand [YouTube id=”video id goes in here”] shortcodes (this one is made up as well, although YouTube shortcodes do exist out in the wild):

using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace Sitecore.Sandbox.Pipelines.ExpandShortcodes
{
    public class ExpandYouTubeShortcodes : ExpandShortcodesProcessor
    {
        protected override IEnumerable<Shortcode> GetShortcodes(string content)
        {
            if(string.IsNullOrWhiteSpace(content))
            {
                return new List<Shortcode>();
            }

            IList<Shortcode> shortcodes = new List<Shortcode>();
            MatchCollection matches = Regex.Matches(content, @"\", RegexOptions.IgnoreCase);

            foreach (Match match in matches)
            {
                string id = match.Groups[1].Value.Trim();
                if (!string.IsNullOrWhiteSpace(id))
                {
                    shortcodes.Add
                    (
                        new Shortcode
                        {
                            Unexpanded = match.Value,
                            Expanded = string.Format(@"", id)
                        }
                    );
                }
            }

            return shortcodes;
        }
    }
}

Next I built a renderField pipeline processor to invoke our new pipeline when the field is a text field of some sort — yes, all fields in Sitecore are fundamentally strings behind the scenes but I’m referring to Single-Line Text, Multi-Line Text, Rich Text, and the deprecated text fields — to expand our shortcodes:

using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Pipelines.RenderField;

using Sitecore.Sandbox.Pipelines.ExpandShortcodes;

namespace Sitecore.Sandbox.Pipelines.RenderField
{
    public class ExpandShortcodes
    {
        public void Process(RenderFieldArgs args)
        {
            if (!ShouldFieldBeProcessed(args))
            {
                return;
            }

            args.Result.FirstPart = GetExpandedShortcodes(args.Result.FirstPart);
        }

        private static bool ShouldFieldBeProcessed(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.FieldTypeKey, "args.FieldTypeKey");
            string fieldTypeKey = args.FieldTypeKey.ToLower();
            return fieldTypeKey == "text"
                    || fieldTypeKey == "rich text"
                    || fieldTypeKey == "single-line text"
                    || fieldTypeKey == "multi-line text";
        }

        private static string GetExpandedShortcodes(string content)
        {
            Assert.ArgumentNotNull(content, "content");
            ExpandShortcodesArgs args = new ExpandShortcodesArgs { Content = content };
            CorePipeline.Run("expandShortcodes", args);
            return args.Content;
        }
    }
}

I cemented all the pieces together using a Sitecore configuration file — this should go in your /App_Config/Include/ folder:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <expandShortcodes>
        <processor type="Sitecore.Sandbox.Pipelines.ExpandShortcodes.ExpandYouTubeShortcodes, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.ExpandShortcodes.ExpandBigBlueTextShortcodes, Sitecore.Sandbox" />
      </expandShortcodes>
      <renderField>
        <processor type="Sitecore.Sandbox.Pipelines.RenderField.ExpandShortcodes, Sitecore.Sandbox" 
                   patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetTextFieldValue, Sitecore.Kernel']" />
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Let’s see the above code in action.

I created a test item, and added BigBlueText and YouTube shortcodes into two different text fields:

shortcode-item-test

I saved, published, and then navigated to the test item:

shortcode-page-rendered

As you can see, our shortcodes were expanded.

If you have any thoughts on this, or ideas around a better shortcode framework for Sitecore, please share in a comment.


Unlock Sitecore Users’ Items During Logout

$
0
0

The other day I saw a post in one of the SDN forums asking how one could go about building a solution to unlock items locked by a user when he/she logs out of Sitecore.

What immediately came to mind was building a new processor for the logout pipeline — this pipeline can be found at /configuration/sitecore/processors/logout in your Sitecore instance’s Web.config — but had to research how one would programmatically get all Sitecore items locked by the current user.

After a bit of fishing in Sitecore.Kernel.dll and Sitecore.Client.dll, I found a query in Sitecore.Client.dll that will give me all locked items for the current user:

fast-query-locked-items

Now all we need to do is add it into a custom logout pipeline processor:

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

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.Logout;

namespace Sitecore.Sandbox.Pipelines.Logout
{
    public class UnlockMyItems
    {
        public void Process(LogoutArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            UnlockMyItemsIfAny();
        }

        private void UnlockMyItemsIfAny()
        {
            IEnumerable<Item> lockedItems = GetMyLockedItems();
            if (!CanProcess(lockedItems))
            {
                return;
            }

            foreach (Item lockedItem in lockedItems)
            {
                Unlock(lockedItem);
            }
        }

        private static IEnumerable<Item> GetMyLockedItems()
        {
            return Context.ContentDatabase.SelectItems(GetMyLockedItemsQuery());
        }

        private static string GetMyLockedItemsQuery()
        {
            return string.Format("fast://*[@__lock='%\"{0}\"%']", Context.User.Name);
        }

        private static bool CanProcess(IEnumerable<Item> lockedItems)
        {
            return lockedItems != null
                    && lockedItems.Any()
                    && lockedItems.Select(item => item.Locking.HasLock()).Any();
        }

        private void Unlock(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (!item.Locking.HasLock())
            {
                return;
            }

            try
            {
                item.Editing.BeginEdit();
                item.Locking.Unlock();
                item.Editing.EndEdit();
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }
        }
    }
}

The class above grabs all items locked by the current user in the context content database. If none are found, we don’t move forward on processing.

When there are locked items for the current user, the code checks to see if each item is locked before unlocking, just in case some other account unlocks the item before we unlock it — I don’t know what would happen if we try to unlock an item that isn’t locked. If you know, please share in a comment.

I then injected the above pipeline processor into the logout pipeline using the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <logout>
        <processor patch:after="*[@type='Sitecore.Pipelines.Logout.CheckModified, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.Logout.UnlockMyItems, Sitecore.Sandbox"/>
      </logout>
    </processors>
  </sitecore>
</configuration>

Let’s test-drive this.

I first logged into Sitecore using my ‘mike’ account, and chose the Home item to lock:

lets-lock-home-item

It is now locked:

home-is-locked-1

In another session, I logged in using another account, and saw that ‘mike’ had locked the Home item:

home-is-locked-2

I switched back to the other session under the ‘mike’ user, and logged out:

IF

When I logged back in, I saw that the Home item was no longer locked:

item-now-unlocked

If you have any thoughts or suggestions on making this better, please share in a comment below.



Specify the Maximum Width of Images Uploaded to the Sitecore Media Library

$
0
0

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.


Insert a New Item After a Sibling Item in Sitecore

$
0
0

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.


Resolve Media Library Items Linked in Sitecore Aliases

$
0
0

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!


Choose Template Fields to Display in the Sitecore Content Editor

$
0
0

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.


Perform a Virus Scan on Files Uploaded into Sitecore

$
0
0

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.


Viewing all 51 articles
Browse latest View live