Quantcast
Channel: Sitecore – SitecoreJunkie.com
Viewing all 112 articles
Browse latest View live

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.


Replace Proxies With Clones in the Sitecore CMS

$
0
0

The other day I stumbled upon a thread in the Sitecore Developer Network (SDN) forums that briefly touched upon replacing proxies with clones, and I wondered whether anyone had built any sort of tool that creates clones for items being proxied — basically a tool that would automate creating clones from proxies — and removes the proxies once the clones are in place.

Since I am not aware of such a tool — not to mention that I’m hooked on programming and just love coding — I decided to create one.

The following command is my attempt at such a tool:

using System;
using System.Linq;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Data.Proxies;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Commands
{
    public class TransformProxyToClones : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            TransformProxyItemToClones(GetContextItem(context));
        }

        private static void TransformProxyItemToClones(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if(!CanTransform(item))
            {
                return;
            }

            string proxyType = GetProxyType(item);
            Item source = GetItem(GetSourceItemFieldValue(item));
            Item target = GetItem(GetTargetItemFieldValue(item));
            
            if (AreEqualIgnoreCase(proxyType, "Entire sub-tree"))
            {
                DeleteItem(item);
                CloneEntireSubtree(source, target);
            }
            else if (AreEqualIgnoreCase(proxyType, "Root item only"))
            {
                DeleteItem(item);
                CloneRootOnly(source, target);
            }
        }

        private static void CloneEntireSubtree(Item source, Item destination)
        {
            Clone(source, destination, true);
        }
        
        private static void CloneRootOnly(Item root, Item destination)
        {
            Clone(root, destination, false);
        }

        private static Item Clone(Item cloneSource, Item cloneDestination, bool deep)
        {
            Assert.ArgumentNotNull(cloneSource, "cloneSource");
            Assert.ArgumentNotNull(cloneDestination, "cloneDestination");
            return cloneSource.CloneTo(cloneDestination, deep);
        }

        private static void DeleteItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (Settings.RecycleBinActive)
            {
                item.Recycle();
            }
            else
            {
                item.Delete();
            }
        }

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

            return CommandState.Hidden;
        }

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

        private static bool CanTransform(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return IsProxy(item)
                    && IsSourceDatabaseFieldEmpty(item)
                    && !string.IsNullOrWhiteSpace(GetProxyType(item))
                    && !string.IsNullOrWhiteSpace(GetSourceItemFieldValue(item))
                    && !string.IsNullOrWhiteSpace(GetTargetItemFieldValue(item));
        }

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

        private static string GetProxyType(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item["Proxy type"];
        }

        private static bool IsSourceDatabaseFieldEmpty(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return string.IsNullOrWhiteSpace(item["Source database"]);
        }

        private static string GetSourceItemFieldValue(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item["Source item"];
        }

        private static string GetTargetItemFieldValue(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item["Target item"];
        }
        
        private static Item GetItem(string path)
        {
            Assert.ArgumentNotNullOrEmpty(path, "path");
            return Context.ContentDatabase.GetItem(path);
        }

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

The above command is only visible for proxy items having both source and target items set, and the proxy is for the context content database.

When the command is invoked, the source item — conjoined with its descendants if its sub-tree is also being proxied — is cloned to the target item, after the proxy definition item is deleted.

I registered the above command in Sitecore using an include configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:transformproxytoclones" type="Sitecore.Sandbox.Commands.TransformProxyToClones, Sitecore.Sandbox"/>
    </commands>
  </sitecore>
</configuration>

I also wired this up in the core database for the item context menu (I’ve omitted a screenshot on how this is done; If you would like to see how this is done, please see part 1 and part 2 of my post showing how to add to the item context menu).

Let’s take this for a drive.

I created a bunch of items for testing:

proxy-test-tree-created-items

I then created a proxy for these test items:

proxy-item-sub-tree

I then right-clicked on our test proxy item to launch its context menu, and then clicked on the “Transform Proxy To Clones” menu option:

transform-proxy-to-clones-context

The proxy item was removed, and we now have clones:

proxy-gone-now-clones

If you can think of any other ideas around proxies or clones, or know of other tools that create clones from proxies, please leave a comment.


Tailor Sitecore Item Web API Field Values On Read

$
0
0

Last week Sitecore MVP Kamruz Jaman asked me in this tweet if I could answer this question on Stack Overflow.

The person asking the question wanted to know why alt text for images aren’t returned in responses from the Sitecore Item Web API, and was curious if it were possible to include these.

After digging around the Sitecore.ItemWebApi.dll and my local copy of /App_Config/Include/Sitecore.ItemWebApi.config — this config file defines a bunch of pipelines and their processors that can be augmented or overridden — I learned field values are returned via logic in the Sitecore.ItemWebApi.Pipelines.Read.GetResult class, which is exposed in /configuration/sitecore/pipelines/itemWebApiRead/processor[@type="Sitecore.ItemWebApi.Pipelines.Read.GetResult, Sitecore.ItemWebApi"] in /App_Config/Include/Sitecore.ItemWebApi.config:

sitecore-item-webapi-raw-field-value

This is an example of a raw value for an image field — it does not include the alt text for the image:

image-xml-content-editor

I spun up a copy of the console application written by Kern Herskind Nightingale — Director of Technical Services at Sitecore UK — to show the value returned by the above pipeline processor for an image field:

image-xml-out-of-the-box

The Sitecore.ItemWebApi.Pipelines.Read.GetResult class exposes a virtual method hook — the protected method GetFieldInfo() — that allows custom code to change a field’s value before it is returned.

I wrote the following class as an example for changing an image field’s value:

using System;
using System.IO;
using System.Web;
using System.Web.UI;

using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Read;
using Sitecore.Web.UI.WebControls;

using HtmlAgilityPack;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Read
{
    public class EnsureImageFieldAltText : GetResult
    {
        protected override Dynamic GetFieldInfo(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            Dynamic dynamic = base.GetFieldInfo(field);
            AddAltTextForImageField(dynamic, field);
            return dynamic;
        }

        private static void AddAltTextForImageField(Dynamic dynamic, Field field)
        {
            Assert.ArgumentNotNull(dynamic, "dynamic");
            Assert.ArgumentNotNull(field, "field");

            if(IsImageField(field))
            {
                dynamic["Value"] = AddAltTextToImages(field.Value, GetAltText(field));
            }
        }

        private static string AddAltTextToImages(string imagesXml, string altText)
        {
            if (string.IsNullOrWhiteSpace(imagesXml) || string.IsNullOrWhiteSpace(altText))
            {
                return imagesXml;
            }
            
            HtmlDocument htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(imagesXml);
            HtmlNodeCollection images = htmlDocument.DocumentNode.SelectNodes("//image");
            foreach (HtmlNode image in images)
            {
                if (image.Attributes["src"] != null)
                {
                    image.SetAttributeValue("src", GetAbsoluteUrl(image.GetAttributeValue("src", string.Empty)));
                }
                
                image.SetAttributeValue("alt", altText);
            }
            
            return htmlDocument.DocumentNode.InnerHtml;
        }

        private static string GetAbsoluteUrl(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            Uri uri = HttpContext.Current.Request.Url;

            if (url.StartsWith(uri.Scheme))
            {
                return url;
            }

            string port = string.Empty;

            if (uri.Port != 80)
            {
                port = string.Concat(":", uri.Port);
            }

            return string.Format("{0}://{1}{2}/~{3}", uri.Scheme, uri.Host, port, VirtualPathUtility.ToAbsolute(url));
        }

        private static string GetAltText(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            if (IsImageField(field))
            {
                ImageField imageField = field;
                if (imageField != null)
                {
                    return imageField.Alt;
                }
            }

            return string.Empty;
        }

        private static bool IsImageField(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            return field.Type == "Image";
        }
    }
}

The class above — with the help of the Sitecore.Data.Fields.ImageField class — gets the alt text for the image, and adds a new alt XML attribute to the XML before it is returned.

The class also changes the relative url defined in the src attribute in to be an absolute url.

I then swapped out /configuration/sitecore/pipelines/itemWebApiRead/processor[@type="Sitecore.ItemWebApi.Pipelines.Read.GetResult, Sitecore.ItemWebApi"] with the class above in /App_Config/Include/Sitecore.ItemWebApi.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- Lots of stuff here -->
      <!-- Handles the item read operation. -->
		<itemWebApiRead>
			<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Read.EnsureImageFieldAltText, Sitecore.Sandbox" />
		</itemWebApiRead>
		  <!--Lots of stuff here too -->
		
    </pipelines>
	<!-- Even more stuff here -->
  </sitecore>
</configuration>

I then reran the console application to see what the XML now looks like, and as you can see the new alt attribute was added:

alt-image-xml

You might be thinking “Mike, image field XML values are great in Sitecore’s Content Editor, but client code consuming this data might have trouble with it. Is there anyway to have HTML be returned instead of XML?

You bet!

The following subclass of Sitecore.ItemWebApi.Pipelines.Read.GetResult returns HTML, not XML:

using System;
using System.IO;
using System.Web;
using System.Web.UI;

using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Read;
using Sitecore.Web.UI.WebControls;

using HtmlAgilityPack;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Read
{
    public class TailorFieldValue : GetResult
    {
        protected override Dynamic GetFieldInfo(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            Dynamic dynamic = base.GetFieldInfo(field);
            TailorValueForImageField(dynamic, field);
            return dynamic;
        }

        private static void TailorValueForImageField(Dynamic dynamic, Field field)
        {
            Assert.ArgumentNotNull(dynamic, "dynamic");
            Assert.ArgumentNotNull(field, "field");

            if (field.Type == "Image")
            {
                dynamic["Value"] = SetAbsoluteUrlsOnImages(GetImageHtml(field));
            }
        }

        private static string SetAbsoluteUrlsOnImages(string html)
        {
            if (string.IsNullOrWhiteSpace(html))
            {
                return html;
            }

            HtmlDocument htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(html);
            HtmlNodeCollection images = htmlDocument.DocumentNode.SelectNodes("//img");
            foreach (HtmlNode image in images)
            {
                if (image.Attributes["src"] != null)
                {
                    image.SetAttributeValue("src", GetAbsoluteUrl(image.GetAttributeValue("src", string.Empty)));
                }
            }

            return htmlDocument.DocumentNode.InnerHtml;
        }

        private static string GetAbsoluteUrl(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            Uri uri = HttpContext.Current.Request.Url;

            if (url.StartsWith(uri.Scheme))
            {
                return url;
            }

            string port = string.Empty;

            if (uri.Port != 80)
            {
                port = string.Concat(":", uri.Port);
            }

            return string.Format("{0}://{1}{2}{3}", uri.Scheme, uri.Host, port, VirtualPathUtility.ToAbsolute(url));
        }

        private static string GetImageHtml(Field field)
        {
            return GetImageHtml(field.Item, field.Name);
        }

        private static string GetImageHtml(Item item, string fieldName)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNullOrEmpty(fieldName, "fieldName");
            return RenderImageControlHtml(new Image { Item = item, Field = fieldName });
        }

        private static string RenderImageControlHtml(Image image)
        {
            Assert.ArgumentNotNull(image, "image");
            string html = string.Empty;

            using (TextWriter textWriter = new StringWriter())
            {
                using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(textWriter))
                {
                    image.RenderControl(htmlTextWriter);
                }

                html = textWriter.ToString();
            }

            return html;
        }
    }
}

The class above uses an instance of the Image field control (Sitecore.Web.UI.WebControls.Image) to do all the work for us around building the HTML for the image, and we also make sure the url within it is absolute — just as we had done above.

I then wired this up to my local Sitecore instance in /App_Config/Include/Sitecore.ItemWebApi.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- Lots of stuff here -->
      <!-- Handles the item read operation. -->
		<itemWebApiRead>
			<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Read.TailorFieldValue, Sitecore.Sandbox" />
		</itemWebApiRead>
		  <!--Lots of stuff here too -->
		
    </pipelines>
	<!-- Even more stuff here -->
  </sitecore>
</configuration>

I then executed the console application, and was given back HTML for the image:

image-html-returned

If you can think of other reasons for manipulating field values in subclasses of Sitecore.ItemWebApi.Pipelines.Read.GetResult, please drop a comment.

Addendum
Kieran Marron — a Lead Developer at Sitecore — wrote another Sitecore.ItemWebApi.Pipelines.Read.GetResult subclass example that returns an image’s alt text in the Sitecore Item Web API response via a new JSON property. Check it out!


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.


Encrypt Web Forms For Marketers Fields in Sitecore

$
0
0

In an earlier post, I walked you through how I experimented with data encryption of field values in Sitecore, and alluded to how I had done a similar thing for the Web Forms For Marketers (WFFM) module on a past project at work.

Months have gone by, and guilt has begun to gnaw away at my entire being — no, not really, I’m exaggerating a bit — but I definitely have been feeling bad for not sharing a solution.

In order to shake feeling bad, I decided to put my nose to the grindstone over the past few days to come up with a different solution than the one I had built at work, and this post shows the fruits of that labor.

I decided to reuse the interface I had created in my older post on data encryption. I am re-posting it here for reference:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace Sitecore.Sandbox.Security.Encryption.Base
{
    public interface IEncryptor
    {
        string Encrypt(string input);
 
        string Decrypt(string input);
    }
}

I then asked myself “What encryption algorithm should I use?” I scavenged through the System.Security.Cryptography namespace in mscorlib.dll using .NET Reflector, and discovered some classes, when used together, achieve data encryption using the RC2 algorithm — an algorithm I know nothing about:

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

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Security.Encryption.Base;

namespace Sitecore.Sandbox.Security.Encryption
{
    public class RC2Encryptor : IEncryptor
    {
        public string Key { get; set; }

        private RC2Encryptor(string key)
        {
            SetKey(key);
        }

        private void SetKey(string key)
        {
            Assert.ArgumentNotNullOrEmpty(key, "key");
            Key = key;
        }

        public string Encrypt(string input)
        {
            return Encrypt(input, Key);
        }

        public static string Encrypt(string input, string key)
        {
            byte[] inputArray = UTF8Encoding.UTF8.GetBytes(input);
            RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();
            rc2.Key = UTF8Encoding.UTF8.GetBytes(key);
            rc2.Mode = CipherMode.ECB;
            rc2.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = rc2.CreateEncryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
            rc2.Clear();
            return System.Convert.ToBase64String(resultArray, 0, resultArray.Length);
        }

        public string Decrypt(string input)
        {
            return Decrypt(input, Key);
        }

        public static string Decrypt(string input, string key)
        {
            byte[] inputArray = System.Convert.FromBase64String(input);
            RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();
            rc2.Key = UTF8Encoding.UTF8.GetBytes(key);
            rc2.Mode = CipherMode.ECB;
            rc2.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = rc2.CreateDecryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
            rc2.Clear();
            return UTF8Encoding.UTF8.GetString(resultArray);
        }

        public static IEncryptor CreateNewRC2Encryptor(string key)
        {
            return new RC2Encryptor(key);
        }
    }
}

As I had mentioned in my previous post on data encryption, I am not a cryptography expert, nor a security expert.

I am not aware of how strong the RC2 encryption algorithm is, or what it would take to crack it. I strongly advise against using this algorithm in any production system without first consulting with a security expert. I am using it in this post only as an example.

If you happen to be a security expert, or are able to compare encryption algorithms defined in the System.Security.Cryptography namespace in mscorlib.dll, please share in a comment.

In a previous post on manipulating field values for WFFM forms, I had to define a new class that implements Sitecore.Forms.Data.IField in Sitecore.Forms.Core.dll in order to change field values — it appears the property mutator for the “out of the box” class is ignored — and decided to reuse it here:

using System;

using Sitecore.Forms.Data;

namespace Sitecore.Sandbox.Utilities.Manipulators.DTO
{
    public class WFFMField : IField
    {
        public string Data { get; set; }

        public Guid FieldId { get; set; }

        public string FieldName { get; set; }

        public IForm Form { get; set; }

        public Guid Id { get; internal set; }

        public string Value { get; set; }
    }
}

Next, I created a WFFM Data Provider that encrypts and decrypts field names and values:

using System;
using System.Collections.Generic;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Forms.Data;
using Sitecore.Forms.Data.DataProviders;
using Sitecore.Reflection;

using Sitecore.Sandbox.Security.DTO;
using Sitecore.Sandbox.Security.Encryption.Base;
using Sitecore.Sandbox.Security.Encryption;
using Sitecore.Sandbox.Utilities.Manipulators.DTO;

namespace Sitecore.Sandbox.WFFM.Forms.Data.DataProviders
{
    public class WFFMEncryptionDataProvider : WFMDataProviderBase
    {
        private WFMDataProviderBase InnerProvider { get; set; }
        private IEncryptor Encryptor { get; set; }

        public WFFMEncryptionDataProvider(string innerProvider)
            : this(CreateInnerProvider(innerProvider), CreateDefaultEncryptor())
        {
        }

        public WFFMEncryptionDataProvider(string connectionString, string innerProvider)
            : this(CreateInnerProvider(innerProvider, connectionString), CreateDefaultEncryptor())
        {
        }

        public WFFMEncryptionDataProvider(WFMDataProviderBase innerProvider)
            : this(innerProvider, CreateDefaultEncryptor())
        {
        }

        public WFFMEncryptionDataProvider(WFMDataProviderBase innerProvider, IEncryptor encryptor)
        {
            SetInnerProvider(innerProvider);
            SetEncryptor(encryptor);
        }

        private static WFMDataProviderBase CreateInnerProvider(string innerProvider, string connectionString = null)
        {
            Assert.ArgumentNotNullOrEmpty(innerProvider, "innerProvider");
            if (!string.IsNullOrWhiteSpace(connectionString))
            {
                return ReflectionUtil.CreateObject(innerProvider, new[] { connectionString }) as WFMDataProviderBase;
            }

            return ReflectionUtil.CreateObject(innerProvider, new object[0]) as WFMDataProviderBase;
        }
        
        private void SetInnerProvider(WFMDataProviderBase innerProvider)
        {
            Assert.ArgumentNotNull(innerProvider, "innerProvider");
            InnerProvider = innerProvider;
        }

        private static IEncryptor CreateDefaultEncryptor()
        {
            return DataNullTerminatorEncryptor.CreateNewDataNullTerminatorEncryptor(GetEncryptorSettings());
        }

        private static DataNullTerminatorEncryptorSettings GetEncryptorSettings()
        {
            return new DataNullTerminatorEncryptorSettings
            {
                EncryptionDataNullTerminator = Settings.GetSetting("WFFM.Encryption.DataNullTerminator"),
                InnerEncryptor = RC2Encryptor.CreateNewRC2Encryptor(Settings.GetSetting("WFFM.Encryption.Key"))
            };
        }

        private void SetEncryptor(IEncryptor encryptor)
        {
            Assert.ArgumentNotNull(encryptor, "encryptor");
            Encryptor = encryptor;
        }

        public override void ChangeStorage(Guid formItemId, string newStorage)
        {
            InnerProvider.ChangeStorage(formItemId, newStorage);
        }

        public override void ChangeStorageForForms(IEnumerable<Guid> ids, string storageName)
        {
            InnerProvider.ChangeStorageForForms(ids, storageName);
        }

        public override void DeleteForms(IEnumerable<Guid> formSubmitIds)
        {
            InnerProvider.DeleteForms(formSubmitIds);
        }

        public override void DeleteForms(Guid formItemId, string storageName)
        {
            InnerProvider.DeleteForms(formItemId, storageName);
        }

        public override IEnumerable<IPool> GetAbundantPools(Guid fieldId, int top, out int total)
        {
            return InnerProvider.GetAbundantPools(fieldId, top, out total);
        }

        public override IEnumerable<IForm> GetForms(QueryParams queryParams, out int total)
        {
            IEnumerable<IForm> forms = InnerProvider.GetForms(queryParams, out total);
            DecryptForms(forms);
            return forms;
        }

        public override IEnumerable<IForm> GetFormsByIds(IEnumerable<Guid> ids)
        {
            IEnumerable<IForm> forms = InnerProvider.GetFormsByIds(ids);
            DecryptForms(forms);
            return forms;
        }

        public override int GetFormsCount(Guid formItemId, string storageName, string filter)
        {
            return InnerProvider.GetFormsCount(formItemId, storageName, filter);
        }

        public override IEnumerable<IPool> GetPools(Guid fieldId)
        {
            return InnerProvider.GetPools(fieldId);
        }

        public override void InsertForm(IForm form)
        {
            EncryptForm(form);
            InnerProvider.InsertForm(form);
        }

        public override void ResetPool(Guid fieldId)
        {
            InnerProvider.ResetPool(fieldId);
        }

        public override IForm SelectSingleForm(Guid fieldId, string likeValue)
        {
            IForm form = InnerProvider.SelectSingleForm(fieldId, likeValue);
            DecryptForm(form);
            return form;
        }

        public override bool UpdateForm(IForm form)
        {
            EncryptForm(form);
            return InnerProvider.UpdateForm(form);
        }

        private void EncryptForms(IEnumerable<IForm> forms)
        {
            Assert.ArgumentNotNull(forms, "forms");
            foreach (IForm form in forms)
            {
                EncryptForm(form);
            }
        }

        private void EncryptForm(IForm form)
        {
            Assert.ArgumentNotNull(form, "form");
            Assert.ArgumentNotNull(form.Field, "form.Field");
            form.Field = EncryptFields(form.Field);
        }

        private IEnumerable<IField> EncryptFields(IEnumerable<IField> fields)
        {
            Assert.ArgumentNotNull(fields, "fields");
            IList<IField> encryptedFields = new List<IField>();
            foreach (IField field in fields)
            {
                encryptedFields.Add(EncryptField(field));
            }

            return encryptedFields;
        }

        private IField EncryptField(IField field)
        {
            Assert.ArgumentNotNull(field, "field");
            return CreateNewWFFMField(field, Encrypt(field.FieldName), Encrypt(field.Value));
        }

        private void DecryptForms(IEnumerable<IForm> forms)
        {
            Assert.ArgumentNotNull(forms, "forms");
            foreach (IForm form in forms)
            {
                DecryptForm(form);
            }
        }

        private void DecryptForm(IForm form)
        {
            Assert.ArgumentNotNull(form, "form");
            Assert.ArgumentNotNull(form.Field, "form.Field");
            form.Field = DecryptFields(form.Field);
        }

        private IEnumerable<IField> DecryptFields(IEnumerable<IField> fields)
        {
            Assert.ArgumentNotNull(fields, "fields");
            IList<IField> decryptedFields = new List<IField>();
            foreach (IField field in fields)
            {
                decryptedFields.Add(DecryptField(field));
            }

            return decryptedFields;
        }

        private IField DecryptField(IField field)
        {
            Assert.ArgumentNotNull(field, "field");
            return CreateNewWFFMField(field, Decrypt(field.FieldName), Decrypt(field.Value));
        }

        private string Encrypt(string input)
        {
            return Encryptor.Encrypt(input);
        }

        private string Decrypt(string input)
        {
            return Encryptor.Decrypt(input);
        }

        private static IField CreateNewWFFMField(IField field, string fieldName, string value)
        {
            if (field != null)
            {
                return new WFFMField
                {
                    Data = field.Data,
                    FieldId = field.FieldId,
                    FieldName = fieldName,
                    Form = field.Form,
                    Id = field.Id,
                    Value = value
                };
            }

            return null;
        }
    }
}

The above class employs the decorator pattern. An inner WFFM Data Provider — which is supplied via a parameter configuration node in \App_Config\Include\forms.config, and is created via magic within the Sitecore.Reflection.ReflectionUtil class — is wrapped.

Methods that save and retrieve form data in the above Data Provider decorate the same methods defined on the inner WFFM Data Provider.

Methods that save form data pass form(s) — and eventually their fields — through a chain of Encrypt methods. The Encrypt method that takes in an IField instance as an argument encrypts the instance’s field name and value, and returns a new instance of the WFFMField class using the encrypted data and the other properties on the IField instance untouched.

Similarly, a chain of Decrypt methods are called for form(s) being retrieved from the inner Data Provider — field names and values are decrypted and saved into a new instance of the WFFMField class, and the manipulated form(s) are returned.

I want to point out that the IEncryptor instance is actually an instance of DataNullTerminatorEncryptor — see my earlier post on data encryption to see how this is implemented — which decorates our RC2Encryptor. This decorating encryptor stamps encrypted strings with a special string so we don’t accidentally encrypt a value twice, and it also won’t try to decrypt a string value that isn’t encrypted.

I added a new include configuration file to hold encryption related settings — the IEncryptor’s key, and the string that will be put at the end of all encrypted data via the DataNullTerminatorEncryptor instance:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <!-- TODO: change the terminator so it does not scream "PLEASE TRY TO CRACK ME!" -->
      <setting name="WFFM.Encryption.DataNullTerminator" value="#I_AM_ENCRYPTED#" />

      <!-- I found this key somewhere on the internet, so it must be secure -->
      <setting name="WFFM.Encryption.Key" value="88bca90e90875a" />
    </settings>
  </sitecore>
</configuration>

I then hooked in the encryption WFFM Data Provider in \App_Config\Include\forms.config, and set the type for the inner provider:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
  
	<!-- There is stuff up here -->
  
	<!-- MS SQL -->
	<formsDataProvider type="Sitecore.Sandbox.WFFM.Forms.Data.DataProviders.WFFMEncryptionDataProvider, Sitecore.Sandbox">
		<!-- No, this is not my real connection string -->
		<param desc="connection string">user id=(user);password=(password);Data Source=(database)</param>
		<param desc="inner provider">Sitecore.Forms.Data.DataProviders.WFMDataProvider, Sitecore.Forms.Core</param>
	</formsDataProvider>

	<!-- There is stuff down here -->
	
	</sitecore>
</configuration>

Let’s see this in action.

I created a new WFFM form with some fields for testing:

the-form-in-sitecore

I then mapped the above form to a new page in Sitecore, and published both the form and page.

I navigated to the form page, and filled it out:

the-form-page

After clicking submit, I was given a ‘thank you’ page’:

the-form-page-confirmation

Let’s see what our field data looks like in the WFFM database:

the-form-submission-encrypted-db

As you can see, the data is encrypted.

Now, let’s see if the data is also encrypted in the Forms Report for our test form:

forms-report-encryption

As you can see, the end-user would be unaware that any data manipulation is happening under the hood.

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


Change the Data Serialization Format in the Sitecore Item Web API

$
0
0

I had a major urge yesterday to continue my tinkering of the Sitecore Item Web API, and wondered how one would go about changing the serialization format of its response.

Without re-reading the documentation on how one could go about doing this — I forgot that this was discussed in its documentation (check out pages 16-17 in the Sitecore Item Web API 1.0.0 Developer’s Guide where you can see how to use an XML serializer) — I tackled this by experimentation, and came up with a slightly different solution than the one offered in the Developer’s Guide.

I considered using an XML serializer for this blog post, but decided to fish around on the internet to see what other data serialization formats exist, and discovered YAML — a format that looks similar to JSON.

I continued my internet surfing — I mean research — and found a .NET library — YamlDotNet — that assists developers in converting objects into YAML, and decided to give it a go.

The following Sitecore item Web API Serializer uses this YAML library:

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

using Sitecore.Diagnostics;
using Sitecore.ItemWebApi.Serialization;

using YamlDotNet.RepresentationModel.Serialization;

namespace Sitecore.Sandbox.ItemWebApi.Serialization
{
    public class YamlSerializer : ISerializer
    {
        // Serializer in YamlDotNet.RepresentationModel.Serialization
        private static readonly Serializer Serializer = new Serializer();

        public YamlSerializer()
        {
        }

        public string Serialize(object value)
        {
            Assert.ArgumentNotNull(value, "value");
            string yaml = string.Empty;
            using(StringWriter stringWriter = new StringWriter())
            {
                Serializer.Serialize(stringWriter, value);
                yaml = stringWriter.ToString();
            }

            return yaml;
        }

        public string SerializedDataMediaType
        {
            get
            {
                return "application/x-yaml";
            }
        }
    }
}

The Serializer above just delegates responsibility to the YamlDotNet’s YAML serializer.

Now that we have our YamlSerializer ready to go, we have to somehow wire it up to the Sitecore Item Web API.

After some digging in Sitecore.ItemWebApi.dll, I learned the out of the box JsonSerializer is set in a preprocessRequest pipeline processor:

JsonSerializer

Following this lead, I created a custom preprocessRequest pipeline processor for setting our new YamlSerializer:

using System;
using System.Web;

using Sitecore.Pipelines.PreprocessRequest;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi.Serialization;

using Sitecore.Sandbox.ItemWebApi.Serialization;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.PreprocessRequest
{
    public class ResolveSerializer : PreprocessRequestProcessor
    {
        public override void Process(PreprocessRequestArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            Assert.ArgumentNotNull(arguments.Context, "arguments.Context");
            Assert.ArgumentNotNull(arguments.Context.Request, "arguments.Context.Request");
            ISerializer serializer = GetSerializer(arguments.Context.Request);
            if (serializer != null)
            {
                Sitecore.ItemWebApi.Context.Current.Serializer = serializer;
            }
        }

        private static ISerializer GetSerializer(HttpRequest request)
        {
            if(IsYamlRequest(request))
            {
                return new YamlSerializer();
            }

            return null;
        }

        private static bool IsYamlRequest(HttpRequest request)
        {
            Assert.ArgumentNotNull(request, "request");
            return string.Equals(request["format"], "yaml", StringComparison.CurrentCultureIgnoreCase);
        }
    }
}

We only set our YamlSerializer when client code requests data to be returned as YAML — this is made known when the client code sets “Yaml” in a HTTP request parameter named “format” (an example of this would be &format=Yaml via a query string).

I then added the new preprocessRequest pipeline processor in \App_Config\Include\Sitecore.ItemWebApi.config, and made sure it’s called right after /configuration/sitecore/pipelines/preprocessRequest/processor[@type="Sitecore.ItemWebApi.Pipelines.PreprocessRequest.RewriteUrl, Sitecore.ItemWebApi"] — default instances are set here on the Sitecore Item Web API’s Context instance, and we should ensure this object exits before changing properties on it:

<?xml version="1.0" encoding="utf-8"?>
	<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
		<sitecore>
			<pipelines>
			<!-- there is stuff up here -->
				<preprocessRequest>
					<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.PreprocessRequest.ResolveSerializer, Sitecore.Sandbox" patch:before="processor[@type='Sitecore.Pipelines.PreprocessRequest.CheckIgnoreFlag, Sitecore.Kernel']" />
					<processor type="Sitecore.ItemWebApi.Pipelines.PreprocessRequest.RewriteUrl, Sitecore.ItemWebApi" patch:before="processor[@type='Sitecore.Sandbox.ItemWebApi.Pipelines.PreprocessRequest.ResolveSerializer, Sitecore.Sandbox']" />
				</preprocessRequest>
			<!-- and more stuff down here -->
		</sitecore>
	</configuration>

After modifying some code in my copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK — I added &format=yaml as a query string parameter — I invoked it to retrieve an item in my local instance of Sitecore:

yaml-output

As you can see, the response is now in YAML.

If you have any thoughts on this, or have any recommendations on using other serialization formats, 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! :)


Add JavaScript to the Client OnClick Event of the Sitecore WFFM Submit Button

$
0
0

A SDN forum thread popped up a week and a half ago asking whether it were possible to attach a Google Analytics event to the WFFM submit button — such would involve adding a snippet of JavaScript to the OnClick attribute of the WFFM submit button’s HTML — and I was immediately curious how one would go about achieving this, and whether this were possible at all.

I did a couple of hours of research last night — I experimented with custom processors of pipelines used by WFFM — but found no clean way of adding JavaScript to the OnClick event of the WFFM submit button.

However — right before I was about to throw in the towel for the night — I did find a solution on how one could achieve this — albeit not necessarily a clean solution since it involves some HTML manipulation (I would opine using the OnClientClick attribute of an ASP.NET Button to be cleaner, but couldn’t access the WFFM submit button due to its encapsulation and protection level in a WFFM WebControl) — via a custom Sitecore.Form.Core.Renderings.FormRender:

using System.IO;
using System.Linq;
using System.Web.UI;

using Sitecore.Form.Core.Renderings;

using HtmlAgilityPack;

namespace Sitecore.Sandbox.Form.Core.Renderings
{
    public class AddOnClientClickFormRender : FormRender
    {
        private const string ConfirmJavaScriptFormat = "if(!confirm('Are you sure you want to submit this form?')) {{ return false; }} {0} ";

        protected override void DoRender(HtmlTextWriter output)
        {
            string html = string.Empty;
            using (StringWriter stringWriter = new StringWriter())
            {
                using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(stringWriter))
                {
                    base.DoRender(htmlTextWriter);
                }

                html = AddOnClientClickToSubmitButton(stringWriter.ToString());
            }

            output.Write(html);
        }

        private static string AddOnClientClickToSubmitButton(string html)
        {
            if (string.IsNullOrWhiteSpace(html))
            {
                return html;
            }

            HtmlNode submitButton = GetSubmitButton(html);
            if (submitButton == null && submitButton.Attributes["onclick"] != null)
            {
                return html;
            }

            submitButton.Attributes["onclick"].Value = string.Format(ConfirmJavaScriptFormat, submitButton.Attributes["onclick"].Value);
            return submitButton.OwnerDocument.DocumentNode.InnerHtml;
        }

        private static HtmlNode GetSubmitButton(string html)
        {
            HtmlNode documentNode = GetHtmlDocumentNode(html);
            return documentNode.SelectNodes("//input[@type='submit']").FirstOrDefault();
        }

        private static HtmlNode GetHtmlDocumentNode(string html)
        {
            HtmlDocument htmlDocument = CreateNewHtmlDocument(html);
            return htmlDocument.DocumentNode;
        }

        private static HtmlDocument CreateNewHtmlDocument(string html)
        {
            HtmlDocument htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(html);
            return htmlDocument;
        }
    }
}

The FormRender above uses Html Agility Pack — which comes with Sitecore — to retrieve the submit button in the HTML that is constructed by the base FormRender class, and adds a snippet of JavaScript to the beginning of the OnClick attribute (there is already JavaScript in this attribute, and we want to run our JavaScript first).

I didn’t wire up a Google Analytics event to the submit button in this FormRender — it would’ve required me to spin up an account for my local sandbox instance, and I feel this would’ve been overkill for this post.

Instead — as an example of adding JavaScript to the OnClick attribute of the WFFM submit button — I added code to launch a JavaScript confirmation dialog asking the form submitter whether he/she would like to continue submitting the form. If the user clicks the ‘Cancel’ button, the form is not submitted, and is submitted if the user clicks ‘OK’.

I then had to hook this custom FormRender to the WFFM Form Rendering — /sitecore/layout/Renderings/Modules/Web Forms for Marketers/Form — in Sitecore:

form-rendering

I then saved, published, and navigated to a WFFM test form. I then clicked the submit button:

confirmation-box

As you can see, I was prompted with a JavaScript confirmation dialog box.

If you have any thoughts on this implementation, or know of a better way to do this, please drop a comment.

Until next time, have a Sitecorelicious day! :)


Delete An Item Across Multiple Databases in Sitecore

$
0
0

Have you ever thought “wouldn’t it be handy to have the ability to delete an item across multiple databases in Sitecore?” In other words, wouldn’t it be nice to not have to publish the parent of an item — with sub-items — after deleting it, just to remove it from a target database?

This particular thought has crossed my mind more than once, and I decided to do something about it. This post showcases what I’ve done.

I spent some time surfing through Sitecore.Kernel.dll and Sitecore.Client.dll in search of a dialog that allows users to select multiple options simultaneously but came up shorthanded — if you are aware of one, please leave a comment — so I had to roll my own:

<?xml version="1.0" encoding="utf-8" ?> 
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
	<DeleteInDatabases>
		<FormDialog ID="DeleteInDatabasesDialog" Icon="Business/32x32/data_delete.png" Header="Delete Item In Databases" 
		  Text="Select the databases where you want to delete the item." OKButton="Delete">
		  
		  <CodeBeside Type="Sitecore.Sandbox.Shell.Applications.Dialogs.DeleteInDatabasesForm,Sitecore.Sandbox"/>
		  <GridPanel Width="100%" Height="100%" Style="table-layout:fixed">
			<Border Padding="4" ID="Databases"/>
		  </GridPanel>
		</FormDialog>
	</DeleteInDatabases>
</control>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;
using System.Web.UI.HtmlControls;

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

namespace Sitecore.Sandbox.Shell.Applications.Dialogs
{
    public class DeleteInDatabasesForm : DialogForm
    {
        private const string DatabaseCheckboxIDPrefix = "db_";

        protected Border Databases;

        private string _ItemId;
        protected string ItemId
        {
            get
            {
                if (string.IsNullOrWhiteSpace(_ItemId))
                {
                    _ItemId = WebUtil.GetQueryString("id");
                }

                return _ItemId;
            }
        }

        protected override void OnLoad(EventArgs e)
        {
            AddDatabaseCheckboxes();
            base.OnLoad(e);
        }

        private void AddDatabaseCheckboxes()
        {
            Databases.Controls.Clear();
            foreach (string database in GetDatabasesForSelection())
            {
                HtmlGenericControl checkbox = new HtmlGenericControl("input");
                Databases.Controls.Add(checkbox);
                checkbox.Attributes["type"] = "checkbox";
                checkbox.Attributes["value"] = database;
                string checkboxId = string.Concat(DatabaseCheckboxIDPrefix, database);
                checkbox.ID = checkboxId;
                HtmlGenericControl label = new HtmlGenericControl("label");
                Databases.Controls.Add(label);
                label.Attributes["for"] = checkboxId;
                label.InnerText = database;
                Databases.Controls.Add(new LiteralControl("<br>"));
            }
        }

        private static IEnumerable<string> GetDatabasesForSelection()
        {
            return WebUtil.GetQueryString("db").Split("|".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
        }

        protected override void OnOK(object sender, EventArgs args)
        {
            IEnumerable<string> selectedDatabases = GetSelectedDabases();
            if (!selectedDatabases.Any())
            {
                SheerResponse.Alert("Please select at least one database!");
                return;
            }

            DeleteItemInDatabases(selectedDatabases, ItemId);
            SheerResponse.Alert("The item has been deleted in all selected databases!");
            base.OnOK(sender, args);
        }

        private static IEnumerable<string> GetSelectedDabases()
        {
            IList<string> databases = new List<string>();
            foreach (string id in Context.ClientPage.ClientRequest.Form.Keys)
            {
                if (!string.IsNullOrWhiteSpace(id) && id.StartsWith(DatabaseCheckboxIDPrefix))
                {
                    databases.Add(id.Substring(3));
                }
            }

            return databases;
        }

        private static void DeleteItemInDatabases(IEnumerable<string> databases, string itemId)
        {
            foreach(string database in databases)
            {
                DeleteItemInDatabase(database, itemId);
            }
        }

        private static void DeleteItemInDatabase(string databaseName, string itemId)
        {
            Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            Assert.ArgumentNotNullOrEmpty(itemId, "itemId");
            Database database = Factory.GetDatabase(databaseName);
            Assert.IsNotNull(database, "Invalid database!");
            DeleteItem(database.GetItem(itemId));
        }

        private static void DeleteItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (Settings.RecycleBinActive)
            {
                item.Recycle();
            }
            else
            {
                item.Delete();
            }
        }
    }
}

The dialog above takes in an item’s ID — this is the ID of the item the user has chosen to delete across multiple databases — and a list of databases a user can choose from as checkboxes.

Ideally the item should exist in each database, albeit the code will throw an exception via an assertion in the case when client code supplies a database, the user selects it, and the item does not live in it.

If the user does not check off one checkbox, and clicks the ‘Delete’ button, an ‘Alert’ box will let the user know s/he must select at least one database.

When databases are selected, and the ‘Delete’ button is clicked, the item will be deleted — or put into the Recycle Bin — in all selected databases.

Now we need a way to launch this dialog. I figured it would make sense to have it be available from the item context menu — just as the ‘Delete’ menu option is available there “out of the box” — and built the following command for it:

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

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Text;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Commands
{
    public class DeleteInDatabases : Command
    {
        public override void Execute(CommandContext commandContext)
        {
            Context.ClientPage.Start(this, "ShowDialog", CreateNewClientPipelineArgs(GetItem(commandContext)));
        }

        private static ClientPipelineArgs CreateNewClientPipelineArgs(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            ClientPipelineArgs args = new ClientPipelineArgs();
            args.Parameters["ItemId"] = item.ID.ToString();
            args.Parameters["ParentId"] = item.ParentID.ToString();
            return args;
        }

        private void ShowDialog(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                SheerResponse.ShowModalDialog
                (
                    GetDialogUrl
                    (
                        GetDatabasesForItem(args.Parameters["ItemId"]), 
                        args.Parameters["ItemId"]
                    ),
                    "300px",
                    "500px",
                    string.Empty,
                    true
               );

               args.WaitForPostBack();
            }
            else
            {
                RefreshChildren(args.Parameters["ParentId"]);
            }
        }

        private void RefreshChildren(string parentId)
        {
            Assert.ArgumentNotNullOrEmpty(parentId, "parentId");
            Context.ClientPage.SendMessage(this, string.Format("item:refreshchildren(id={0})", parentId));
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            bool shouldEnable = Context.User.IsAdministrator
                                && IsInDatabasesOtherThanCurrentContent(GetItem(commandContext));

            if (shouldEnable)
            {
                return CommandState.Enabled;
            }

            return CommandState.Hidden;
        }

        private static bool IsInDatabasesOtherThanCurrentContent(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return GetDatabasesForItem(item.ID.ToString()).Count() > 1;
        }
        private static Item GetItem(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            Assert.ArgumentNotNull(commandContext.Items, "commandContext.Items");
            return commandContext.Items.FirstOrDefault();
        }

        private static IEnumerable<string> GetDatabasesForItemExcludingContentDB(string id)
        {
            return GetDatabasesForItem(id).Where(db => string.Equals(db, Context.ContentDatabase.Name, StringComparison.CurrentCultureIgnoreCase));
        }

        private static IEnumerable<string> GetDatabasesForItem(string id)
        {
            Assert.ArgumentNotNullOrEmpty(id, "id");
            return (from database in Factory.GetDatabases()
                    let itemInDatabase = database.GetItem(id)
                    where itemInDatabase != null
                    select database.Name).ToList();
        }

        private static string GetDialogUrl(IEnumerable<string> databases, string id)
        {
            Assert.ArgumentNotNullOrEmpty(id, "id");
            Assert.ArgumentNotNull(databases, "databases");
            Assert.ArgumentCondition(databases.Any(), "databases", "At least one database should be supplied!");
            UrlString urlString = new UrlString(UIUtil.GetUri("control:DeleteInDatabases"));
            urlString.Append("id", id);
            urlString.Append("db", string.Join("|", databases));
            return urlString.ToString();
        }
    }
}

The command is only visible when the item is in another database other than the context content database and the user is an admin.

When the item context menu option is clicked, the command passes a pipe delimited list of database names — only databases that contain the item — and the item’s ID to the dialog through its query string.

Once the item is deleted via the dialog, control is returned back to the command, and it then refreshes all siblings of the deleted item — this is done so the deleted item is removed from the content tree if the context content database was chosen in the dialog.

I then made this command available in Sitecore using a configuration include file:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:DeleteInDatabases" type="Sitecore.Sandbox.Commands.DeleteInDatabases,Sitecore.Sandbox"/>
    </commands>
  </sitecore>
</configuration>

I’ve omitted the step on how I’ve wired this up 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 this in action.

I navigated to a test item that lives in the master and web databases, and launched its item context menu:

context-menu-delete-in-dbs

I clicked the ‘Delete in Databases’ menu option, and was presented with this dialog:

delete-in-db-1

I got excited and forgot to select a database before clicking the ‘Delete’ button:

delete-in-db-2

I then selected all databases, and clicked ‘Delete’:

delete-in-db-3

When the dialog closed, we can see that our test item is gone:

item-vanished

Rest assured, it’s in the Recycle Bin:

delete-in-db-4

It was also deleted in the web database as well — I’ve omitted screenshots of this since they would be identical to the last two screenshots above.

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

Until next time, have a Sitecoretastic day!


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.


Delete Associated Files on the Filesystem of Sitecore Items Deleted From the Recycle Bin

$
0
0

Last week a question was asked in one of the SDN forums on how one should go about deleting files on the filesystem that are associated with Items that are permanently deleted from the Recycle Bin — I wasn’t quite clear on what the original poster meant by files being linked to Items inside of Sitecore, but I assumed this relationship would be defined somewhere, or somehow.

After doing some research, I reckoned one could create a new command based on Sitecore.Shell.Framework.Commands.Archives.Delete in Sitecore.Kernel.dll to accomplish this:

Sitecore.Shell.Framework.Commands.Archives.Delete

However, I wasn’t completely satisfied with this approach, especially when it would require a substantial amount of copying and pasting of code — a practice that I vehemently abhor — and decided to seek out a different, if not better, way of doing this.

From my research, I discovered that one could just create his/her own Archive class — it would have to ultimately derive from Sitecore.Data.Archiving.Archive in Sitecore.Kernel — which would delete a file on the filesystem associated with a Sitecore Item:

using System;
using System.IO;
using System.Linq;
using System.Web;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Archiving;
using Sitecore.Data.DataProviders.Sql;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Data.Archiving
{
    public class FileSystemHookSqlArchive : SqlArchive
    {
        private static readonly string FolderPath = GetFolderPath();

        public FileSystemHookSqlArchive(string name, Database database)
            : base(name, database)
        {
        }

        public override void RemoveEntries(ArchiveQuery query)
        {
            DeleteFromFileSystem(query);
            base.RemoveEntries(query);
        }

        protected virtual void DeleteFromFileSystem(ArchiveQuery query)
        {
            if (query.ArchivalId == Guid.Empty)
            {
                return;
            }

            Guid itemId = GetItemId(query.ArchivalId);
            if (itemId == Guid.Empty)
            {
                return;
            }

            string filePath = GetFilePath(itemId.ToString());
            if (string.IsNullOrWhiteSpace(filePath))
            {
                return;
            }

            TryDeleteFile(filePath);
        }

        private void TryDeleteFile(string filePath)
        {
            try
            {
                if (File.Exists(filePath))
                {
                    File.Delete(filePath);
                }
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }
        }

        public virtual Guid GetItemId(Guid archivalId)
        {
            if (archivalId == Guid.Empty)
            {
                return Guid.Empty;
            }
            
            ArchiveQuery query = new ArchiveQuery
            {
                ArchivalId = archivalId
            };

            SqlStatement selectStatement = GetSelectStatement(query, "{0}ItemId{1}");
            if (selectStatement == null)
            {
                return Guid.Empty;
            }
            return GetGuid(selectStatement.Sql, selectStatement.GetParameters(), Guid.Empty);
        }

        private Guid GetGuid(string sql, object[] parameters, Guid defaultValue)
        {
            using (DataProviderReader reader = Api.CreateReader(sql, parameters))
            {
                if (!reader.Read())
                {
                    return defaultValue;
                }
                return Api.GetGuid(0, reader);
            }
        }

        private static string GetFilePath(string fileName)
        {
            string filePath = Directory.GetFiles(FolderPath, string.Concat(fileName, "*.*")).FirstOrDefault();
            if (!string.IsNullOrWhiteSpace(filePath))
            {
                return filePath;    
            }

            return string.Empty;
        }

        private static string GetFolderPath()
        {
            return HttpContext.Current.Server.MapPath(Settings.GetSetting("FileSystemHookSqlArchive.Folder"));
        }
    }
}

In the subclass of Sitecore.Data.Archiving.SqlArchive above — I’m using Sitecore.Data.Archiving.SqlArchive since I’m using SqlServer for my Sitecore instance — I try to find a file that is named after its associated Item’s ID — minus the curly braces — in a folder that I’ve mapped in a configuration include file (see below).

I first have to get the Item’s ID from the database using the supplied ArchivalId — this is all the calling code gives us, so we have to make do with what we have.

If the file exists, we try to delete it — we do this before letting the base class delete the Item from Recycle Bin so that we can retrieve the Item’s ID from the database before it’s removed from the Archive database table — and log any errors we encounter upon exception.

I then hooked in an instance of the above Archive class in a custom Sitecore.Data.Archiving.ArchiveProvider class:

using System.Xml;

using Sitecore.Data;
using Sitecore.Data.Archiving;
using Sitecore.Xml;

namespace Sitecore.Sandbox.Data.Archiving
{
    public class FileSystemHookSqlArchiveProvider : SqlArchiveProvider
    {
        protected override Archive GetArchive(XmlNode configNode, Database database)
        {
            string attribute = XmlUtil.GetAttribute("name", configNode);
            if (string.IsNullOrEmpty(attribute))
            {
                return null;
            }

            return new FileSystemHookSqlArchive(attribute, database);
        }
    }
}

The above class — which derives from Sitecore.Data.Archiving.SqlArchiveProvider since I’m using SqlServer — only overrides its base class’s GetArchive factory method. We instantiate an instance of our Archive class instead of the “out of the box” Sitecore.Data.Archiving.SqlArchive class within it.

I then had to replace the “out of the box” Sitecore.Data.Archiving.ArchiveProvider reference, and define the location of our files in the following configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <archives defaultProvider="sql" enabled="true">
      <providers>
        <add name="sql" patch:instead="add[@type='Sitecore.Data.Archiving.SqlArchiveProvider, Sitecore.Kernel']" type="Sitecore.Sandbox.Data.Archiving.FileSystemHookSqlArchiveProvider, Sitecore.Sandbox" database="*"/>
      </providers>
    </archives>
    <settings>
      <setting name="FileSystemHookSqlArchive.Folder" value="/test/" />
    </settings>
  </sitecore>
</configuration>

Let’s test this out.

I first created a test Item to delete:

test-item-to-delete

I then had to create a test file on the filesystem in my test folder — the test folder lives in my Sitecore instance’s website root:

test-folder-with-test-file

I deleted the test Item from the content tree, opened up the Recycle Bin, selected the test Item, and got an itchy trigger finger — I want to delete the Item forever :) :

delete-file-forever

After clicking the Delete button, I saw that the file on the filesystem was deleted as well:

file-was-deleted

If you have any thoughts on this, or recommendations around making it better, 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.


Expand Tokens on Sitecore Items Using a PowerShell Function in Sitecore PowerShell Extensions

$
0
0

During my Sitecore from the Command Line presentation at the Sitecore User Group – New England, I had briefly showcased a custom PowerShell function that expands Sitecore tokens in fields of a supplied item, and how I had saved this function into the Functions section of the Script Library — /sitecore/system/Modules/PowerShell/Script Library/Functions — of the Sitecore PowerShell Extensions module. This blog post captures what I had shown.

This is the custom function I had shown — albeit I changed its name to adhere to the naming convention in PowerShell for functions and commands (Verb-SingularNoun):

function Expand-SitecoreToken {
	<#
        .SYNOPSIS
             Expand tokens on the supplied item
              
        .EXAMPLE
            Expand tokens on the home item.
             
            PS master:\> Get-Item "/sitecore/content/home" | Expand-SitecoreToken
    #>
	[CmdletBinding()]
    param( 
		[ValidateNotNull()]
		[Parameter(ValueFromPipeline=$True)]
        [Sitecore.Data.Items.Item]$item
    )
	
    $item.Editing.BeginEdit()
    
    Try
    {
        $tokenReplacer = [Sitecore.Configuration.Factory]::GetMasterVariablesReplacer()
        $tokenReplacer.ReplaceItem($item)
        $result = $item.Editing.EndEdit()
        "Expanded tokens on item " + $item.Paths.Path
    }
    Catch [system.Exception]
    {
        $item.Editing.CancelEdit()
        "Failed to expand tokens on item"
        "Reason: " + $error
    }
}

The function above calls Sitecore.Configuration.Factory.GetMasterVariablesReplacer() for an instance of the MasterVariablesReplacer class — which is defined and can be overridden in the “MasterVariablesReplacer” setting in your Sitecore instance’s Web.config — and passes the item supplied to the function to the MasterVariablesReplacer instance’s ReplaceItem() method after the item has been put into editing mode.

Once tokens have been expanded, a confirmation message is sent to the Results window.

If an exception is caught, we display it — the exception is captured in the $error global variable.

I saved the above function into the Script Library of my copy of the Sitecore PowerShell Extensions module:

spe-save-function

An item was created in the Script Library to house the function:

Expand-SitecoreToken-item

Let’s try it out.

Let’s expand tokens on the Home item:

spe-home-unexpanded-tokens

In the Integrated Scripting Environment of the Sitecore PowerShell Extensions module, I typed in the following code:

Execute-Script "master:/system/Modules/PowerShell/Script Library/Functions/Expand-SitecoreToken"
Get-Item . | Expand-SitecoreToken

You can consider the Execute-Script “master:/system/Modules/PowerShell/Script Library/Functions/Expand-SitecoreToken” line of code to be comparable to a javascript “script” tag — it will execute the script thus defining the function so we can execute it.

I then ran that code above:

excuted-Expand-SitecoreToken-on-home

Once the script finished running, I went back over to the Content Editor, and saw that tokens were expanded on the Home item:

spe-function-home-tokens-expanded

You might be thinking “Mike, I really don’t want to be bothered with expanding these tokens, and would rather have our Content Editors/Authors do it. is there something we can set up to make that happen?”

You bet. :)

In the Sitecore PowerShell Extension module, you can save PowerShell into the Script Library to be executed via an item context menu option click. All you have to do is save it into the Script Library under the Content Editor Context Menu item:

spe-context-menu-option

The script is then saved in a new item created under Content Editor Context Menu item in the Script Library:

spe-content-menu-option-item

Let’s see it in action.

I chose the following page at random to expand tokens:

spe-inner-page-three-unexpanded-tokens

I right-clicked on the item to launch it’s context menu, opened up scripts, and saw a new “Expand Tokens” option:

spe-new-context-menu-option

I clicked it, and was given a dialog with a status bar:

spe-context-menu-expanding-tokens

I refreshed the item, and saw that all tokens were expanded:

spe-context-menu-tokens-expanded

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

Until next time, have a scriptabulous day!


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.


Viewing all 112 articles
Browse latest View live