Home » 2012 » December

Monthly Archives: December 2012

Advertisements

Have a Field Day With Custom Sitecore Fields

This week, I’ve been off from work, and have been spending most of my downtime experimenting with Sitecore-ry development things — no, not you Mike — primarily poking around the core database to find things I can override or extend — for a thought provoking article about whether to override or extend things in Sitecore, check out John West’s blog post on this topic — and decided get my hands dirty by creating a custom Sitecore field.

This is something I feel isn’t being done enough by developers out in the wild — myself included — so I decided to take a stab at it, and share how you could go about accomplishing this.

The first field I created was a custom Single-Line Text field with a clear button. Since the jQuery library is available in the Sitecore client, I utilized a jQuery plugin to help me with this. It’s not a very robust plugin — I did have to make a couple of changes due to issues I encountered which I will omit from this article — albeit the purpose of me creating this field was to see how difficult it truly is.

As you can see below, creating a custom Single-Line Text isn’t difficult at all. All one has to do is subclass Sitecore.Shell.Applications.ContentEditor.Text in Sitecore.Kernel.dll, and ultimately override the DoRender() method:

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

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class ClearableSingleLineText : Sitecore.Shell.Applications.ContentEditor.Text
    {
        protected override void DoRender(HtmlTextWriter output)
        {
            SetWidthAndHeightStyle();
            RenderMyHtml(output);
            RenderChildren(output);
            RenderAutoSizeJavascript(output);
        }

        private void RenderMyHtml(HtmlTextWriter output)
        {
            const string htmlFormat = "<input{0}/>";
            output.Write(string.Format(htmlFormat, ControlAttributes));
        }

        private void RenderAutoSizeJavascript(HtmlTextWriter output)
        {
			/* I'm calling the jQuery() function directly since $() is defined for prototype.js in the Sitecore client */
            const string jsFormat = "<script type=\"text/javascript\">jQuery('#{0}').clearable();</script>";
            output.Write(string.Format(jsFormat, ID));
        }
    }
}

I then added a script reference to the jQuery plugin above, and embedded some css in /sitecore/shell/Applications/Content Manager/Default.aspx:

<script type="text/javaScript" language="javascript" src="/sitecore/shell/Controls/Lib/jQuery/jquery.clearable.js"></script>

<style type="text/css">
/* Most of this css was provided by the jquery.clearable plugin author */
a.clearlink 
{
	background: url("/img/close-button.png") no-repeat scroll 0 0 transparent;
	background-position: center center;
	cursor: pointer;
	display: -moz-inline-stack;
	display: inline-block;
	zoom:1;
	*display:inline;	
	height: 12px;
	width: 12px;
	z-index: 2000;
	border: 0px solid;
	position: relative;
	top: -18px;
	left: -5px;
	float: right;
}

a.clearlink:hover 
{
	background: url("/img/close-button.png") no-repeat scroll -12px 0 transparent;
	background-position: center center;
}
</style>

What I did above doesn’t sit well with me. If a future version of Sitecore makes any changes to this .aspx, these changes might be lost (well, hopefully you would keep a copy of this file in a source control system somewhere).

However, I could not think of a better way of adding this javascript and css. If you know of a better way, please leave a comment.

I then defined my library of controls for my custom fields — not much of a library, since I only defined one field so far 🙂 — in a new patch config file /App_Config/Include/CustomFields.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<controlSources>
			<source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="contentcustom"/>
		</controlSources>
	  </sitecore>
</configuration>

In the core database, I created a new field type for my custom Single-Line Text:

clearable single-line text field type

I then added a new field to my template using this new custom field definition in my master database:

added clearable single-line text field

Let’s see this new custom field in action.

I typed in some text on my item, followed by clicking the clear button:

clearable single-line text field test 1

As expected, the text that I had typed was removed from the field:

clearable single-line text field test 2

After having done the above, I wanted to see if I could create a different type of custom field — creating a custom Text field was way too easy, and I wanted something more challenging. I figured creating a custom Multilist field might offer a challenge, so I decided to give it a go.

What I came up with was a custom Multilist field containing Sitecore users, instead of Items as options. I don’t know if there is any practicality in creating such a field, but I decided to go with it just for the purpose of creating a custom Multilist field.

First, I created a class to delegate responsibility for getting selected/unselected users in my field — I creating this class just in case I ever wanted to reuse this in a future custom field that should also contain Sitecore users.

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

using Sitecore.Security.Accounts;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Base
{
    public interface IUsersField
    {
        IEnumerable<User> GetSelectedUsers();

        IEnumerable<User> GetUnselectedUsers();

        string GetProviderUserKey(User user);
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Security;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Security.Accounts;
using Sitecore.Text;

using Sitecore.Sandbox.Shell.Applications.ContentEditor.Base;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class UsersField : IUsersField
    {
        private static readonly string DomainParameterName = Settings.GetSetting("UsersField.DomainParameterName");

        private ListString _SelectedUsers;
        private ListString SelectedUsers
        {
            get
            {
                if (_SelectedUsers == null)
                {
                    _SelectedUsers = new ListString(Value);
                }

                return _SelectedUsers;
            }
        }

        private IEnumerable<User> _UsersInDomain;
        private IEnumerable<User> UsersInDomain
        {
            get
            {
                if (_UsersInDomain == null)
                {
                    _UsersInDomain = GetUsersInDomain();
                }

                return _UsersInDomain;
            }
        }

        private IEnumerable<User> _Users;
        private IEnumerable<User> Users
        {
            get
            {
                if(_Users == null)
                {
                    _Users = GetUsers();
                }

                return _Users;
            }
        }

        private string _Domain;
        private string Domain
        {
            get
            {
                if (string.IsNullOrEmpty(_Domain))
                {
                    _Domain = FieldSettings[DomainParameterName];
                }

                return _Domain;
            }
        }

        private UrlString _FieldSettings;
        private UrlString FieldSettings
        {
            get
            {
                if (_FieldSettings == null)
                {
                    _FieldSettings = GetFieldSettings();
                }

                return _FieldSettings;
            }
        }

        private string Source { get; set; }
        private string Value { get; set; }

        private UsersField(string source, string value)
        {
            SetSource(source);
            SetValue(value);
        }

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

        private void SetValue(string value)
        {
            Value = value;
        }

        private IEnumerable<User> GetUsersInDomain()
        {
            if (!string.IsNullOrEmpty(Domain))
            {
                return Users.Where(user => IsUserInDomain(user, Domain));
            }

            return Users;
        }

        private static IEnumerable<User> GetUsers()
        {
            IEnumerable<User> users = UserManager.GetUsers();

            if (users != null)
            {
                return users;
            }

            return new List<User>();
        }

        private static bool IsUserInDomain(User user, string domain)
        {
            Assert.ArgumentNotNull(user, "user");
            Assert.ArgumentNotNullOrEmpty(domain, "domain");

            string userNameLowerCase = user.Profile.UserName.ToLower();
            string domainLowerCase = domain.ToLower();

            return userNameLowerCase.StartsWith(domainLowerCase);
        }

        private UrlString GetFieldSettings()
        {
            try
            {
                if (!string.IsNullOrEmpty(Source))
                {
                    return new UrlString(Source);
                }
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }
            
            return new UrlString();
        }

        public IEnumerable<User> GetSelectedUsers()
        {
            IList<User> selectedUsers = new List<User>();

            foreach (string providerUserKey in SelectedUsers)
            {
                User selectedUser = UsersInDomain.Where(user => GetProviderUserKey(user) == providerUserKey).FirstOrDefault();
                if (selectedUser != null)
                {
                    selectedUsers.Add(selectedUser);
                }
            }

            return selectedUsers;
        }

        public IEnumerable<User> GetUnselectedUsers()
        {
            IList<User> unselectedUsers = new List<User>();

            foreach (User user in UsersInDomain)
            {
                if (!IsUserSelected(user))
                {
                    unselectedUsers.Add(user);
                }
            }

            return unselectedUsers;
        }

        private bool IsUserSelected(User user)
        {
            string providerUserKey = GetProviderUserKey(user);
            return IsUserSelected(providerUserKey);
        }

        private bool IsUserSelected(string providerUserKey)
        {
            return SelectedUsers.IndexOf(providerUserKey) > -1;
        }

        public string GetProviderUserKey(User user)
        {
            Assert.ArgumentNotNull(user, "user");
            MembershipUser membershipUser = Membership.GetUser(user.Profile.UserName);
            return membershipUser.ProviderUserKey.ToString();
        }

        public static IUsersField CreateNewUsersField(string source, string value)
        {
            return new UsersField(source, value);
        }
    }
}

I then modified my patch config file defined above (/App_Config/Include/CustomFields.config) with a new setting — a setting that specifies the parameter name for filtering on users’ Sitecore domain:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<controlSources>
			<source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="contentcustom"/>
		</controlSources>
		<settings>
			<!-- Parameter name for users' Sitecore domain -->
			<setting name="UsersField.DomainParameterName" value="Domain" />
		</settings>
	  </sitecore>
</configuration>

I then created a new subclass of Sitecore.Shell.Applications.ContentEditor.MultilistEx — I ascertained this to be the class used by the Multilist field in Sitecore by looking at /sitecore/system/Field types/List Types/Multilist field type in the core database:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Security;
using System.Web.UI;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Resources;
using Sitecore.Security.Accounts;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Text;

using Sitecore.Sandbox.Shell.Applications.ContentEditor.Base;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class UsersMultilist : MultilistEx
    {
        private IUsersField _UsersField;
        private IUsersField UsersField
        {
            get
            {
                if (_UsersField == null)
                {
                    _UsersField = CreateNewUsersField();
                }

                return _UsersField;
            }
        }

        public UsersMultilist() 
        {
        }

        protected override void DoRender(HtmlTextWriter output)
        {
            Assert.ArgumentNotNull(output, "output");
            
            SetIDProperty();
            string disabledAttribute = string.Empty;

            if (ReadOnly)
            {
                disabledAttribute = " disabled=\"disabled\"";
            }

            output.Write(string.Format("<input id=\"{0}_Value\" type=\"hidden\" value=\"{1}\" />", ID, StringUtil.EscapeQuote(Value)));
            output.Write(string.Format("<table{0}>", GetControlAttributes()));
            output.Write("<tr>");
            output.Write(string.Format("<td class=\"scContentControlMultilistCaption\" width=\"50%\">{0}</td>", GetAllLabel()));
            output.Write(string.Format("<td width=\"20\">{0}</td>", Images.GetSpacer(20, 1)));
            output.Write(string.Format("<td class=\"scContentControlMultilistCaption\" width=\"50%\">{0}</td>", GetSelectedLabel()));
            output.Write(string.Format("<td width=\"20\">{0}</td>", Images.GetSpacer(20, 1), "</td>"));
            output.Write("</tr>");
            output.Write("<tr>");
            output.Write("<td valign=\"top\" height=\"100%\">");
            output.Write(string.Format("<select id=\"{0}_unselected\" class=\"scContentControlMultilistBox\" multiple=\"multiple\"{1} size=\"10\" ondblclick=\"javascript:scContent.multilistMoveRight('{2}')\" onchange=\"javascript:document.getElementById('{3}_all_help').innerHTML=this.selectedIndex>=0?this.options[this.selectedIndex].innerHTML:''\">", ID, disabledAttribute, ID, ID));
            
            IEnumerable<User> unselectedUsers = GetUnselectedUsers();
            foreach (User unselectedUser in unselectedUsers)
            {
                output.Write(string.Format("<option value=\"{0}\">{1}</option>", GetProviderUserKey(unselectedUser), unselectedUser.Profile.UserName));
            }

            output.Write("</select>");
            output.Write("</td>");
            output.Write("<td valign=\"top\">");
            RenderButton(output, "Core/16x16/arrow_blue_right.png", string.Format("javascript:scContent.multilistMoveRight('{0}')", ID));
            output.Write("<br />");
            RenderButton(output, "Core/16x16/arrow_blue_left.png", string.Format("javascript:scContent.multilistMoveLeft('{0}')", ID));
            output.Write("</td>");
            output.Write("<td valign=\"top\" height=\"100%\">");
            output.Write(string.Format("<select id=\"{0}_selected\" class=\"scContentControlMultilistBox\" multiple=\"multiple\"{1} size=\"10\" ondblclick=\"javascript:scContent.multilistMoveLeft('{2}')\" onchange=\"javascript:document.getElementById('{3}_selected_help').innerHTML=this.selectedIndex>=0?this.options[this.selectedIndex].innerHTML:''\">", ID, disabledAttribute, ID, ID));

            IEnumerable<User> selectedUsers = GetSelectedUsers();
            foreach (User selectedUser in selectedUsers)
            {
                output.Write(string.Format("<option value=\"{0}\">{1}</option>", GetProviderUserKey(selectedUser), selectedUser.Profile.UserName));
            }

            output.Write("</select>");
            output.Write("</td>");
            output.Write("<td valign=\"top\">");
            RenderButton(output, "Core/16x16/arrow_blue_up.png", string.Format("javascript:scContent.multilistMoveUp('{0}')", ID));
            output.Write("<br />");
            RenderButton(output, "Core/16x16/arrow_blue_down.png", string.Format("javascript:scContent.multilistMoveDown('{0}')", ID));
            output.Write("</td>");
            output.Write("</tr>");
            output.Write("<tr>");
            output.Write("<td valign=\"top\">");
            output.Write(string.Format("<div style=\"border:1px solid #999999;font:8pt tahoma;padding:2px;margin:4px 0px 4px 0px;height:14px\" id=\"{0}_all_help\"></div>", ID));
            output.Write("</td>");
            output.Write("<td></td>");
            output.Write("<td valign=\"top\">");
            output.Write(string.Format("<div style=\"border:1px solid #999999;font:8pt tahoma;padding:2px;margin:4px 0px 4px 0px;height:14px\" id=\"{0}_selected_help\"></div>", ID));
            output.Write("</td>");
            output.Write("<td></td>");
            output.Write("</tr>");
            output.Write("</table>");
        }

        protected void SetIDProperty()
        {
            ServerProperties["ID"] = ID;
        }

        protected static string GetAllLabel()
        {
            return GetLabel("All");
        }

        protected static string GetSelectedLabel()
        {
            return GetLabel("Selected");
        }

        protected static string GetLabel(string key)
        {
            return Translate.Text(key);
        }

        protected IEnumerable<User> GetSelectedUsers()
        {
            return UsersField.GetSelectedUsers();
        }

        protected IEnumerable<User> GetUnselectedUsers()
        {
            return UsersField.GetUnselectedUsers();
        }

        protected string GetProviderUserKey(User user)
        {
            return UsersField.GetProviderUserKey(user);
        }

        // Method "borrowed" from MultilistEx control
        protected void RenderButton(HtmlTextWriter output, string icon, string click)
        {
            Assert.ArgumentNotNull(output, "output");
            Assert.ArgumentNotNull(icon, "icon");
            Assert.ArgumentNotNull(click, "click");
            ImageBuilder builder = new ImageBuilder
            {
                Src = icon,
                Width = 0x10,
                Height = 0x10,
                Margin = "2px"
            };
            if (!ReadOnly)
            {
                builder.OnClick = click;
            }
            output.Write(builder.ToString());
        }
        
        private IUsersField CreateNewUsersField()
        {
            return ContentEditor.UsersField.CreateNewUsersField(Source, Value);
        }
    }
}

Overriding the DoRender() method paved the way for me to insert my custom Multilist options — options containg Sitecore users. Selected users will be saved as a pipe delimitered list of ASP.NET Membership UserIDs.

As I had done for my custom Single-Line Text above, I defined my custom Multilist field in the core database:

Users Multilist field type

In the master database, I added a new field to my template using this new field type:

added users multilist field

Now, let’s take this custom field for a test drive.

I went back to my item and saw that all Sitecore users were available for selection in my new field:

users multilist field test 1

Facetiously imagine that a project manager has just run up to you anxiously lamenting — while breathing rapidly and sweating copiously — we cannot show users in the sitecore domain in our field, only those within the extranet domain — to do so will taint our credibility with our clients as Sitecore experts :). You then put on your superhero cape and proudly proclaim “rest assured, we’ve already baked this domain filtering functionality into our custom Multilist field!”:

users multilist field test 2

I saved my template and navigated back to my item:

users multilist field test 3

I selected a couple of users and saved:

users multilist field test 4

I see their Membership UserIDs are being saved as designed:

users multilist field test 5

My intent on creating the above custom fields was to showcase that the option to create your own fields exists in Sitecore. Why not have yourself a field day by creating your own? 🙂

Advertisements

Get Your House in Order: Create Your Own Subitems Sorting Comparer

This morning, I was fishing around in the core database of my local instance to research my next article — yeah, I know it’s Christmas morning and I should be resting, but unwrapping hidden gems in Sitecore.Kernel beats opening up presents anyday — and discovered that one can easily add his/her own subitems sorting comparer.

After opening up .NET reflector and using Sitecore.Data.Comparers.UpdatedComparer as a model, I created the following Comparer within minutes:

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

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

namespace Sitecore.Sandbox.Data.Comparers
{
    public class ItemNameLengthComparer : ExtractedKeysComparer
    {
        protected override int CompareKeys(IKey keyOne, IKey keyTwo)
        {
            Assert.ArgumentNotNull(keyOne, "keyOne");
            Assert.ArgumentNotNull(keyTwo, "keyTwo");
            return IntegerCompareTo(keyOne.Key, keyTwo.Key);
        }

        protected override int DoCompare(Item itemOne, Item itemTwo)
        {
            return IntegerCompareTo(itemOne.Name.Length, itemTwo.Name.Length);
        }

        private static int IntegerCompareTo(object itemOneNameLength, object itemTwoNameLength)
        {
            return IntegerCompareTo((int)itemOneNameLength, (int)itemTwoNameLength);
        }

        private static int IntegerCompareTo(int itemOneNameLength, int itemTwoNameLength)
        {
            return itemOneNameLength.CompareTo(itemTwoNameLength);
        }

        public override IKey ExtractKey(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return new KeyObj 
            { 
                Item = item, 
                Key = item.Name.Length, 
                Sortorder = item.Appearance.Sortorder 
            };
        }
    }
}

All we have to do is override and add our own custom logic to the DoCompare(), CompareKeys() and ExtractKey() methods defined in the ExtractedKeysComparer base class.

The above Comparer will sort items based on the length of their names — items with shorter names will appear before their siblings with longer names.

Next, I created a new Child Sorting item in my master database — yes, I did say the master database since I learned the hard way (my sorting option wasn’t appearing) that these sorting comparers are to be defined in each database where they are used — for my Comparer:

New Child Sorting

Let’s see how this Comparer fares in the wild.

I first created a handful of test items with different item name lengths in a new parent folder under my Home node:

Test Subitems

I then opened up the Subitems Sorting dialog and saw my new subitems sorting option in the ‘Sorting’ dropdown:

Set Subitems Sort 1

Nervously, I selected my new subitems sorting option and clicked ‘OK’:

Set Subitems Sort 2

I then wiped my brow and exhaled with relief after seeing that it had worked as I intended:

Set Subitems Sort 3

The above is further evidence of how customizable the Sitecore client truly is. Remember, the Sitecore client was built using the same API/technologies we use each and everyday to build websites within it — thus empowering us to extend the client where we see fit.

Happy Holidays! 🙂

Custom Sitecore Rich Text Editor Button: Inserting Dynamic Content

Last Thursday, I stumbled upon an article discussing how to create and add a custom button to the Rich Text Editor in Sitecore. This article referenced an article written by Mark Stiles — his article set the foundation for me to do this very thing last Spring for a client.

Unlike the two articles above, I had to create two different buttons to insert dynamic content — special html containing references to other items in the Sitecore content tree via Item IDs — which I would ‘fix’ via a RenderField pipeline when a user would visit the page containing this special html. I had modeled my code around the ‘Insert Link’ button by delegating to a helper class to ‘expand’ my dynamic content as the LinkManager class does for Sitecore links.

The unfortunate thing is I cannot show you that code – it’s proprietary code owned by a previous employer.

Instead, I decided to build something similar to illustrate how I did this. I will insert special html that will ultimately transform into jQuery UI dialogs.

First, I needed to create items that represent dialog boxes. I created a new template with two fields — one field containing the dialog box’s heading and the other field containing copy that will go inside of the dialog box:

Dialog Content Item Template

I then created three dialog box items with content that we will use later on when testing:

Dialog Content Items

With the help of xml defined in /sitecore/shell/Controls/Rich Text Editor/InsertLink/InsertLink.xml and /sitecore/shell/Controls/Rich Text Editor/InsertImage/InsertImage.xml, I created my new xml control definition in a file named /sitecore/shell/Controls/Rich Text Editor/InsertDialog/InsertDialog.xml:

<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
	<RichText.InsertDialog>
	
		<!-- Don't forget to set your icon 🙂 -->
		<FormDialog Icon="Business/32x32/message.png" Header="Insert a Dialog" Text="Select the dialog content item you want to insert." OKButton="Insert Dialog">
			
			<!-- js reference to my InsertDialog.js script. -->
			<!-- For some strange reason, if the period within the script tag is not present, the dialog form won't work -->
			<script Type="text/javascript" src="/sitecore/shell/Controls/Rich Text Editor/InsertDialog/InsertDialog.js">.</script>
			
			<!-- Reference to my InsertDialogForm class -->
			<CodeBeside Type="Sitecore.Sandbox.RichTextEditor.InsertDialog.InsertDialogForm,Sitecore.Sandbox" />

			<!-- Root contains the ID of /sitecore/content/Dialog Content Items -->
			<DataContext ID="DialogFolderDataContext"  Root="{99B14D44-5A0F-43B6-988E-94197D73B348}" />
			<GridPanel Width="100%" Height="100%" Style="table-layout:fixed">			
				<GridPanel Width="100%" Height="100%" Style="table-layout:fixed" Columns="3" GridPanel.Height="100%">
					<Scrollbox Class="scScrollbox scFixSize" Width="100%" Height="100%" Background="white" Border="1px inset" Padding="0" GridPanel.Height="100%" GridPanel.Width="50%" GridPanel.Valign="top">
						<TreeviewEx ID="DialogContentItems" DataContext="DialogFolderDataContext" Root="true" />
					</Scrollbox>
				</GridPanel>
			</GridPanel>
		</FormDialog>
	</RichText.InsertDialog>
</control>

My dialog form will house one lonely TreeviewEx containing all dialog items in the /sitecore/content/Dialog Content Items folder I created above.

I then created the ‘CodeBeside’ DialogForm class to accompany the xml above:

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

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework;
using Sitecore.Web;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Pages;
using Sitecore.Web.UI.Sheer;
using Sitecore.Web.UI.WebControls;

namespace Sitecore.Sandbox.RichTextEditor.InsertDialog
{
    public class InsertDialogForm : DialogForm
    {
        protected DataContext DialogFolderDataContext;
        protected TreeviewEx DialogContentItems;

        protected string Mode
        {
            get
            {
                string mode = StringUtil.GetString(base.ServerProperties["Mode"]);

                if (!string.IsNullOrEmpty(mode))
                {
                    return mode;
                }

                return "shell";
            }
            set
            {
                Assert.ArgumentNotNull(value, "value");
                base.ServerProperties["Mode"] = value;
            }
        }
        
        protected override void OnLoad(EventArgs e)
        {
            Assert.ArgumentNotNull(e, "e");
            base.OnLoad(e);

            if (!Context.ClientPage.IsEvent)
            {
                Inialize();
            }
        }

        private void Inialize()
        {
            SetMode();
            SetDialogFolderDataContextFromQueryString();
        }

        private void SetMode()
        {
            Mode = WebUtil.GetQueryString("mo");
        }

        private void SetDialogFolderDataContextFromQueryString()
        {
            DialogFolderDataContext.GetFromQueryString();
        }
        
        protected override void OnOK(object sender, EventArgs args)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(args, "args");

            string selectedItemID = GetSelectedItemIDAsString();

            if (string.IsNullOrEmpty(selectedItemID))
            {
                return;
            }

            string selectedItemPath = GetSelectedItemPath();
            string javascriptArguments = string.Format("{0}, {1}", EscapeJavascriptString(selectedItemID), EscapeJavascriptString(selectedItemPath));

            if (IsWebEditMode())
            {
                SheerResponse.SetDialogValue(javascriptArguments);
                base.OnOK(sender, args);
            }
            else
            {
                string closeJavascript = string.Format("scClose({0})", javascriptArguments);
                SheerResponse.Eval(closeJavascript);
            }
        }

        private string GetSelectedItemIDAsString()
        {
            ID selectedID = GetSelectedItemID();

            if (selectedID != ID.Null)
            {
                return selectedID.ToString();
            }

            return string.Empty;
        }

        private ID GetSelectedItemID()
        {
            Item selectedItem = GetSelectedItem();

            if (selectedItem != null)
            {
                return selectedItem.ID;
            }

            return ID.Null;
        }

        private string GetSelectedItemPath()
        {
            Item selectedItem = GetSelectedItem();

            if (selectedItem != null)
            {
                return selectedItem.Paths.FullPath;
            }

            return string.Empty;
        }

        private Item GetSelectedItem()
        {
            return DialogContentItems.GetSelectionItem();
        }

        private static string EscapeJavascriptString(string stringToEscape)
        {
            return StringUtil.EscapeJavascriptString(stringToEscape);
        }
        
        protected override void OnCancel(object sender, EventArgs args)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(args, "args");

            if (IsWebEditMode())
            {
                base.OnCancel(sender, args);
            }
            else
            {
                SheerResponse.Eval("scCancel()");
            }
        }

        private bool IsWebEditMode()
        {
            return string.Equals(Mode, "webedit", StringComparison.InvariantCultureIgnoreCase);
        }
    }
} 

This DialogForm basically sends a selected Item’s ID and path back to the client via the scClose() function defined below in a new file named /sitecore/shell/Controls/Rich Text Editor/InsertDialog/InsertDialog.js:

function GetDialogArguments() {
    return getRadWindow().ClientParameters;
}

function getRadWindow() {
  if (window.radWindow) {
        return window.radWindow;
  }
    
    if (window.frameElement && window.frameElement.radWindow) {
        return window.frameElement.radWindow;
    }
    
    return null;
}

var isRadWindow = true;

var radWindow = getRadWindow();

if (radWindow) { 
  if (window.dialogArguments) { 
    radWindow.Window = window;
  } 
}

function scClose(dialogContentItemId, dialogContentItemPath) {
	// we're passing back an object holding data needed for inserting our special html into the RTE
	var dialogInfo = {
		dialogContentItemId: dialogContentItemId,
		dialogContentItemPath: dialogContentItemPath
	};

	getRadWindow().close(dialogInfo);
}

function scCancel() {
  getRadWindow().close();
}

if (window.focus && Prototype.Browser.Gecko) {
  window.focus();
}

I then had to add a new javascript command in /sitecore/shell/Controls/Rich Text Editor/RichText Commands.js to open my dialog form and map this to a callback function — which I named scInsertDialog to follow the naming convention of other callbacks within this script file — to handle the dialogInfo javascript object above that is passed to it:

RadEditorCommandList["InsertDialog"] = function(commandName, editor, args) {
	scEditor = editor;
	
	editor.showExternalDialog(
		"/sitecore/shell/default.aspx?xmlcontrol=RichText.InsertDialog&la=" + scLanguage,
		null, //argument
		500,
		400,
		scInsertDialog, //callback
		null, // callback args
		"Insert Dialog",
		true, //modal
		Telerik.Web.UI.WindowBehaviors.Close, // behaviors
		false, //showStatusBar
		false //showTitleBar
	);
};

function scInsertDialog(sender, dialogInfo) {
	if (!dialogInfo) {
		return;
	}
	
	// build our special html to insert into the RTE
	var placeholderHtml = "<hr class=\"dialog-placeholder\" style=\"width: 100px; display: inline-block; height: 20px;border: blue 4px solid;\"" 
						+ "data-dialogContentItemId=\"" + dialogInfo.dialogContentItemId +"\" "
						+ "title=\"Dialog Content Path: " + dialogInfo.dialogContentItemPath + "\" />";
					  
	scEditor.pasteHtml(placeholderHtml, "DocumentManager");
}

My callback function builds the special html that will be inserted into the Rich Text Editor. I decided to use a <hr /> tag to keep my html code self-closing — it’s much easier to use a self-closing html tag when doing this, since you don’t have to check whether you’re inserting more special html into preexisting special html. I also did this for the sake of brevity.

I am using the title attribute in my special html in order to assist content authors in knowing which dialog items they’ve inserted into rich text fields. When a dialog blue box is hovered over, a tooltip containing the dialog item’s content path will display.

I’m not completely satisfied with building my special html in this javascript file. It probably should be moved into C# somewhere to prevent the ease of changing it — someone could easily just change it without code compilation, and break how it’s rendered to users. We need this html to be a certain way when ‘fixing’ it in our RenderField pipeline defined later in this article.

I then went into the core database and created a new Rich Text Editor profile under /sitecore/system/Settings/Html Editor Profiles:

Duplicated Rich Text Profile

Next, I created a new Html Editor Button (template: /sitecore/templates/System/Html Editor Profiles/Html Editor Button) using the __Html Editor Button master — wow, I never thought I would use the word master again when talking about Sitecore stuff 🙂 — and set its click and icon fields:

Insert Dialog Html Editor Button 1

Insert Dialog Html Editor Button 2

Now, we need to ‘fix’ our special html by converting it into presentable html to the user. I do this using the following RenderField pipeline:

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

using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Pipelines.RenderField;
using Sitecore.Xml.Xsl;

using HtmlAgilityPack;

namespace Sitecore.Sandbox.Pipelines.RenderField
{
    public class ExpandDialogContent
    {
        public void Process(RenderFieldArgs renderFieldArgs)
        {
            if (ShouldFieldBeProcessed(renderFieldArgs))
            {
                ExpandDialogContentTags(renderFieldArgs);
            }
        }

        private bool ShouldFieldBeProcessed(RenderFieldArgs renderFieldArgs)
        {
            return renderFieldArgs.FieldTypeKey.ToLower() == "rich text";
        }

        private void ExpandDialogContentTags(RenderFieldArgs renderFieldArgs)
        {
            HtmlNode documentNode = GetHtmlDocumentNode(renderFieldArgs.Result.FirstPart);
            HtmlNodeCollection dialogs = documentNode.SelectNodes("//hr[@class='dialog-placeholder']");

            foreach (HtmlNode dialogPlaceholder in dialogs)
            {
                HtmlNode dialog = CreateDialogHtmlNode(dialogPlaceholder);

                if (dialog != null)
                {
                    dialogPlaceholder.ParentNode.ReplaceChild(dialog, dialogPlaceholder);
                }
                else
                {
                    dialogPlaceholder.ParentNode.RemoveChild(dialogPlaceholder);
                }
            }

            renderFieldArgs.Result.FirstPart = documentNode.InnerHtml;
        }

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

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

        private HtmlNode CreateDialogHtmlNode(HtmlNode dialogPlaceholder)
        {
            string dialogContentItemId = dialogPlaceholder.Attributes["data-dialogContentItemId"].Value;
            Item dialogContentItem = TryGetItem(dialogContentItemId);

            if(dialogContentItem != null)
            {
                string heading = dialogContentItem["Dialog Heading"];
                string content = dialogContentItem["Dialog Content"];

                return CreateDialogHtmlNode(dialogPlaceholder.OwnerDocument, heading, content);
            }

            return null;
        }

        private Item TryGetItem(string id)
        {
            try
            {
                return Context.Database.Items[id];
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }

            return null;
        }

        private static HtmlNode CreateDialogHtmlNode(HtmlDocument htmlDocument, string heading, string content)
        {
            if (string.IsNullOrEmpty(content))
            {
                return null;
            }

            HtmlNode dialog = htmlDocument.CreateElement("div");
            dialog.Attributes.Add("class", "dialog");
            dialog.Attributes.Add("title", heading);
            dialog.InnerHtml = content;

            return dialog;
        }
    }
}

The above uses Html Agility Pack for finding all instances of my special <hr /> tags and creates new html that my jQuery UI dialog code expects. If you’re not using Html Agility Pack, I strongly recommend checking it out — it has saved my hide on numerous occasions.

I then inject this RenderField pipeline betwixt others via a new patch include file named /App_Config/Include/ExpandDialogContent.config:

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

Now, it’s time to see if all of my hard work above has paid off.

I set the rich text field on my Sample Item template to use my new Rich Text Editor profile:

RTF source

Using the template above, I created a new test item, and opened its rich text field’s editor:

Opened RTE

I then clicked my new ‘Insert Dialog’ button and saw Dialog items I could choose to insert:

Insert Dialog Dialog Form RTF

Since I’m extremely excited, I decided to insert them all:

Inserted Three Dialogs WYSIWYG

I then forgot which dialog was which — the three uniform blue boxes are throwing me off a bit — so I hovered over the first blue box and saw that it was the first dialog item:

Inserted Three Dialogs WYSIWYG Hover

I then snuck a look at the html inserted — it was formatted the way I expected:

Inserted Three Dialogs HTML

I then saved my item, published and navigated to my test page. My RenderField pipeline fixed the special html as I expected:

Rich Text Button Test

I would like to point out that I had to add link and script tags to reference jQuery UI’s css and js files — including the jQuery library — and initialized my dialogs using the jQuery UI Dialog constructor. I have omitted this code.

This does seem like a lot of code to get something so simple to work.

However, it is worth the effort. Not only will you impress your boss and make your clients happy, you’ll also be dubbed the ‘cool kid’ at parties. You really will, I swear. 🙂

Until next time, have a Sitecoretastic day!

Experiments with Field Data Encryption in Sitecore

Last week, I came up with a design employing the decorator pattern for encrypting and decrypting Web Forms for Marketers form data. My design encapsulates and decorates an instance of Sitecore.Forms.Data.DataProviders.WFMDataProvider — the main data provider that lives in the Sitecore.Forms.Core assembly and is used by the module for saving/retrieving form data from its database — by extending and implementing its base class and interface, respectively, and delegates requests to its public methods using the same method signatures. The “decoration” part occurs before before form data is inserted and updated in the database, and decrypted after being retrieved.

Once I completed this design, I began to ponder whether it would be possible to take this one step further and encrypt/decrypt all field data in Sitecore as a whole.

I knew I would eventually have to do some research to see if this were possible. However, I decided to begin my encryption ventures by building classes that encrypt/decrypt data.

I envisioned stockpiling an arsenal of encryption algorithms. I imagined all encryption algorithms having the same public methods to encrypt and decrypt strings of data, so I created the following interface:

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);
    }
}

Since I am lightyears away from being a cryptography expert — or even considering myself a novice — I had to go fishing on the internet to find an encryption utility class in .NET. I found the following algorithm at http://www.deltasblog.co.uk/code-snippets/basic-encryptiondecryption-c/, and put it within a class that adheres to my IEncryptor interface above:

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
{
    // retrieved from http://www.deltasblog.co.uk/code-snippets/basic-encryptiondecryption-c/

    public class TripleDESEncryptor : IEncryptor
    {
        private string Key { get; set; }

        private TripleDESEncryptor(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);
            TripleDESCryptoServiceProvider tripleDES = new TripleDESCryptoServiceProvider();
            tripleDES.Key = UTF8Encoding.UTF8.GetBytes(key);
            tripleDES.Mode = CipherMode.ECB;
            tripleDES.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = tripleDES.CreateEncryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
            tripleDES.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);
            TripleDESCryptoServiceProvider tripleDES = new TripleDESCryptoServiceProvider();
            tripleDES.Key = UTF8Encoding.UTF8.GetBytes(key);
            tripleDES.Mode = CipherMode.ECB;
            tripleDES.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = tripleDES.CreateDecryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
            tripleDES.Clear();
            return UTF8Encoding.UTF8.GetString(resultArray);
        }

        public static IEncryptor CreateNewTripleDESEncryptor(string key)
        {
            return new TripleDESEncryptor(key);
        }
    }
}

I then started wondering how I could ascertain whether data needed to be encrypted or decrypted. In other words, how could I prevent encrypting data that is already encrypted and, conversely, not decrypting data that isn’t encrypted?

I came up with building a decorator class that looks for a string terminator — a substring at the end of a string — and only encrypts the passed string when this terminating substring is not present, or decrypts only when this substring is found at the end of the string.

I decided to use a data transfer object (DTO) for my encryptor decorator, just in case I ever decide to use this same decorator with any encryption class that implements my encryptor interface. I also send in the encryption key and string terminator via this DTO:

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

using Sitecore.Sandbox.Security.Encryption.Base;

namespace Sitecore.Sandbox.Security.DTO
{
    public class DataNullTerminatorEncryptorSettings
    {
        public string EncryptionDataNullTerminator { get; set; }
        public string EncryptionKey { get; set; }
        public IEncryptor Encryptor { get; set; }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Diagnostics;

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

namespace Sitecore.Sandbox.Security.Encryption
{
    public class DataNullTerminatorEncryptor : IEncryptor
    {
        private DataNullTerminatorEncryptorSettings DataNullTerminatorEncryptorSettings { get; set; }

        private DataNullTerminatorEncryptor(DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings)
        {
            SetDataNullTerminatorEncryptorSettings(dataNullTerminatorEncryptorSettings);
        }

        private void SetDataNullTerminatorEncryptorSettings(DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings)
        {
            Assert.ArgumentNotNull(dataNullTerminatorEncryptorSettings, "dataNullTerminatorEncryptorSettings");
            Assert.ArgumentNotNullOrEmpty(dataNullTerminatorEncryptorSettings.EncryptionDataNullTerminator, "dataNullTerminatorEncryptorSettings.EncryptionDataNullTerminator");
            Assert.ArgumentNotNullOrEmpty(dataNullTerminatorEncryptorSettings.EncryptionKey, "dataNullTerminatorEncryptorSettings.EncryptionKey");
            Assert.ArgumentNotNull(dataNullTerminatorEncryptorSettings.Encryptor, "dataNullTerminatorEncryptorSettings.Encryptor");
            DataNullTerminatorEncryptorSettings = dataNullTerminatorEncryptorSettings;
        }

        public string Encrypt(string input)
        {
            if (!IsEncrypted(input))
            {
                string encryptedInput = DataNullTerminatorEncryptorSettings.Encryptor.Encrypt(input);
                return string.Concat(encryptedInput, DataNullTerminatorEncryptorSettings.EncryptionDataNullTerminator);
            }

            return input;
        }

        public string Decrypt(string input)
        {
            if (IsEncrypted(input))
            {
                input = input.Replace(DataNullTerminatorEncryptorSettings.EncryptionDataNullTerminator, string.Empty);
                return DataNullTerminatorEncryptorSettings.Encryptor.Decrypt(input);
            }

            return input;
        }

        private bool IsEncrypted(string input)
        {
            if (!string.IsNullOrEmpty(input))
                return input.EndsWith(DataNullTerminatorEncryptorSettings.EncryptionDataNullTerminator);

            return false;
        }

        public static IEncryptor CreateNewDataNullTerminatorEncryptor(DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings)
        {
            return new DataNullTerminatorEncryptor(dataNullTerminatorEncryptorSettings);
        }
    }
}

Now that I have two encryptors — the TripleDESEncryptor and DataNullTerminatorEncryptor classes — I thought it would be prudent to create a factory class:

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

using Sitecore.Sandbox.Security.DTO;

namespace Sitecore.Sandbox.Security.Encryption.Base
{
    public interface IEncryptorFactory
    {
        IEncryptor CreateNewTripleDESEncryptor(string encryptionKey);

        IEncryptor CreateNewDataNullTerminatorEncryptor();

        IEncryptor CreateNewDataNullTerminatorEncryptor(string encryptionDataNullTerminator, string encryptionKey);

        IEncryptor CreateNewDataNullTerminatorEncryptor(DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings);
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Configuration;

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

namespace Sitecore.Sandbox.Security.Encryption
{
    public class EncryptorFactory : IEncryptorFactory
    {
        private static volatile IEncryptorFactory _Current;
        private static object lockObject = new Object();

        public static IEncryptorFactory Current
        {
            get 
            {
                if (_Current == null) 
                {
                    lock (lockObject) 
                    {
                        if (_Current == null)
                        {
                            _Current = new EncryptorFactory();
                        }
                    }
                }

                return _Current;
            }
        }

        private EncryptorFactory()
        {
        }

        public IEncryptor CreateNewTripleDESEncryptor(string encryptionKey)
        {
            return TripleDESEncryptor.CreateNewTripleDESEncryptor(encryptionKey);
        }

        public IEncryptor CreateNewDataNullTerminatorEncryptor()
        {
            string encryptionDataNullTerminator = Settings.GetSetting("Encryption.DataNullTerminator");
            string encryptionKey = Settings.GetSetting("Encryption.Key");
            return CreateNewDataNullTerminatorEncryptor(encryptionDataNullTerminator, encryptionKey);
        }

        public IEncryptor CreateNewDataNullTerminatorEncryptor(string encryptionDataNullTerminator, string encryptionKey)
        {
            DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings = new DataNullTerminatorEncryptorSettings 
            { 
                EncryptionDataNullTerminator = encryptionDataNullTerminator,
                EncryptionKey = encryptionDataNullTerminator,
                // we're going to use the TripleDESEncryptor by default
                Encryptor = CreateNewTripleDESEncryptor(encryptionKey)  
            };

            return CreateNewDataNullTerminatorEncryptor(dataNullTerminatorEncryptorSettings);
        }

        public IEncryptor CreateNewDataNullTerminatorEncryptor(DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings)
        {
            return DataNullTerminatorEncryptor.CreateNewDataNullTerminatorEncryptor(dataNullTerminatorEncryptorSettings);
        }
    }
}

I decided to make my factory class be a singleton. I saw no need of having multiple instances of this factory object floating around in memory, and envisioned using this same factory object in a tool I wrote to encrypt all field data in all Sitecore databases. I will discuss this tool at the end of this article.

It’s now time to do some research to see whether I have the ability to hijack the main mechanism for saving/retrieving data from Sitecore.

While snooping around in my Web.config, I saw that the core, master and web databases use a data provider defined at configuration/sitecore/dataProviders/main:

<configuration>
	<sitecore database="SqlServer">

		<!-- More stuff here -->

		<dataProviders>
			<main type="Sitecore.Data.$(database).$(database)DataProvider, Sitecore.Kernel">
				<param connectionStringName="$(1)"/>
				<Name>$(1)</Name>
			</main>

			<!-- More stuff here -->

		<dataProviders>

			<!-- More stuff here -->

	</sitecore>
</configuration>

In my local instance, this points to the class Sitecore.Data.SqlServer.SqlServerDataProvider in Sitecore.Kernel.dll. I knew I had open up .NET Reflector and start investigating.

I discovered that there wasn’t much happening in this class at the field level. I also noticed this class used Sitecore.Data.DataProviders.Sql.SqlDataProvider as its base class, so I continued my research there.

I struck gold in the Sitecore.Data.DataProviders.Sql.SqlDataProvider class. Therein, I found methods that deal with saving/retrieving field data from an instance of the database utility class Sitecore.Data.DataProviders.Sql.SqlDataApi.

I knew this was the object I had to either decorate or override. Since most of the methods I needed to decorate with my encryption functionality were signed as protected, I had to subclass this class in order to get access to them. I then wrapped these methods with calls to my encryptor’s Encrypt(string input) and Decrypt(string input) methods:

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

using Sitecore.Collections;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.DataProviders;
using Sitecore.Data.DataProviders.Sql;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;

using Sitecore.Sandbox.Security.Encryption;
using Sitecore.Sandbox.Security.Encryption.Base;

namespace Sitecore.Sandbox.Data.DataProviders.Sql
{
    public class EncryptionSqlDataProvider : SqlDataProvider
    {
        private static readonly IEncryptorFactory EncryptorBegetter = EncryptorFactory.Current;
        private static readonly IEncryptor Encryptor = EncryptorBegetter.CreateNewDataNullTerminatorEncryptor();

        public EncryptionSqlDataProvider(SqlDataApi sqlDataApi) 
            : base(sqlDataApi)
        {
        }

        public override FieldList GetItemFields(ItemDefinition itemDefinition, VersionUri versionUri, CallContext context)
        {
            FieldList fieldList = base.GetItemFields(itemDefinition, versionUri, context);

            if (fieldList == null)
                return null;

            FieldList fieldListDecrypted = new FieldList(); 
            foreach (KeyValuePair<ID, string> field in fieldList)
            {
                string decryptedValue = Decrypt(field.Value);
                fieldListDecrypted.Add(field.Key, decryptedValue);
            }

            return fieldListDecrypted;
        }

        protected override string GetFieldValue(string tableName, Guid entryId)
        {
            string value = base.GetFieldValue(tableName, entryId);
            return Decrypt(value);
        }

        protected override void SetFieldValue(string tableName, Guid entryId, string value)
        {
            string encryptedValue = Encrypt(value);
            base.SetFieldValue(tableName, entryId, encryptedValue);
        }

        protected override void WriteSharedField(ID itemId, FieldChange change, DateTime now, bool fieldsAreEmpty)
        {
            FieldChange unencryptedFieldChange = GetEncryptedFieldChange(itemId, change);
            base.WriteSharedField(itemId, unencryptedFieldChange, now, fieldsAreEmpty);
        }

        protected override void WriteUnversionedField(ID itemId, FieldChange change, DateTime now, bool fieldsAreEmpty)
        {
            FieldChange unencryptedFieldChange = GetEncryptedFieldChange(itemId, change);
            base.WriteUnversionedField(itemId, unencryptedFieldChange, now, fieldsAreEmpty);
        }

        protected override void WriteVersionedField(ID itemId, FieldChange change, DateTime now, bool fieldsAreEmpty)
        {
            FieldChange unencryptedFieldChange = GetEncryptedFieldChange(itemId, change);
            base.WriteVersionedField(itemId, unencryptedFieldChange, now, fieldsAreEmpty);
        }   

        private FieldChange GetEncryptedFieldChange(ID itemId, FieldChange unencryptedFieldChange)
        {
            if (!string.IsNullOrEmpty(unencryptedFieldChange.Value))
            {
                Field field = GetField(itemId, unencryptedFieldChange.FieldID);
                string encryptedValue = Encrypt(unencryptedFieldChange.Value);
                return new FieldChange(field, encryptedValue);
            }

            return unencryptedFieldChange;
        }

        private Field GetField(ID itemId, ID fieldID)
        {
            Item item = GetItem(itemId);
            return GetField(item, fieldID);
        }

        private Item GetItem(ID itemId)
        {
            return base.Database.Items[itemId];
        }

        private static Field GetField(Item owner, ID fieldID)
        {
            return new Field(fieldID, owner);
        }

        private static string Encrypt(string value)
        {
            return Encryptor.Encrypt(value);
        }

        private static string Decrypt(string value)
        {
            return Encryptor.Decrypt(value);
        }
    }
}

After creating my new EncryptionSqlDataProvider above, I had to define my own SqlServerDataProvider that inherits from it:

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

using Sitecore.Data.SqlServer;

using Sitecore.Sandbox.Data.DataProviders.Sql;

namespace Sitecore.Sandbox.Data.SqlServer
{
    public class EncryptionSqlServerDataProvider : EncryptionSqlDataProvider
    {
        public EncryptionSqlServerDataProvider(string connectionString)
             : base(new SqlServerDataApi(connectionString))
        {
        }
    }
}

I put in a patch reference to my new EncryptionSqlServerDataProvider in a new config file (/App_Config/Include/EncryptionDatabaseProvider.config), and defined the settings used by my encryptors:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<dataProviders>
			<main type="Sitecore.Data.$(database).$(database)DataProvider, Sitecore.Kernel">
				<patch:attribute name="type">Sitecore.Sandbox.Data.$(database).Encryption$(database)DataProvider, Sitecore.Sandbox</patch:attribute>
		       </main>
		</dataProviders>
		<settings>
			<!-- must be 24 characters long -->
			<setting name="Encryption.Key" value="4rgjvqm234gswdfd045r3f4q" />
			
			<!-- this is added to the end of encrypted data in the database -->
			<setting name="Encryption.DataNullTerminator" value="%IS_ENCRYPTED%" />
		</settings>
	</sitecore>
</configuration>

Will the above work, or will I be retreating back to the whiteboard scratching my head while being in a state of bewilderment?. Let’s try out the above to see where my fate lies.

I inserted a new Sample Item under my Home node, entered some content into four fields, and then clicked the Save button:

encryption test item

I opened up my Sitecore master database in Microsoft SQL Server Management Studio — I don’t recommend this being a standard Sitecore development practice, although I am doing this here to see if my encryption scheme worked — to see if my field data was encrypted. It was encrypted successfully:

sql

I then published my item to the web database, and pulled up my new Sample Item page in a browser to make sure it works despite being encrypted in the database:

presentation

Hooray! It works!

You might be asking yourself: how would we go about encrypting “legacy” field data in all Sitecore databases? Well, I wrote tool — the reason for creating my EncryptorFactory above — yesterday to do this. It encrypted all field data in the core, master and web databases.

However, when I opened up the desktop view of the Sitecore client, many of my tray buttons disappeared. Seeing this has lead me to conclude it’s not a good idea to encrypt data in the core database.

Plus, after having rolled back my core database to its previous state — a state of being unencrypted — I tried to open up the Content Editor in the master database. I then received an exception stemming from the rules engine — apparently not all fields in both the master and web databases should be encrypted. There must be code outside of the main data provider accessing data in all three databases.

Future research is needed to discover which fields are good candidates for having their data encrypted, and which fields should be ignored.

Put Things Into Context: Augmenting the Item Context Menu – Part 2

In part 1 of this article, I helped out a friend by walking him through how I added new Publishing menu options — using existing commands in Sitecore — to my Item context menu.

In this final part of the article, I will show you how I created my own custom command and pipeline to copy subitems from one Item in the content tree to another, and wired them up to a new Item context menu option within the ‘Copying’ fly-out menu of the context menu.

Overriding two methods — CommandState QueryState(CommandContext context) and void Execute(CommandContext context) — is paramount when creating a custom Sitecore.Shell.Framework.Commands.Command.

The QueryState() method determines how we should treat the menu option given the current passed context. In other words, should the menu option be enabled, disabled, or hidden? This method serves as a hook — it’s declared virtual — and returns CommandState.Enabled by default. All menu options are enabled by default unless overridden to do otherwise.

The Execute method contains the true “meat and potatoes” of a command’s logic — this method is the place where we do “something” to items passed to it via the CommandContext object. This method is declared abstract in the Command class, ultimately forcing subclasses to define their own logic — there is no default logic for this method.

Here is my command to copy subitems from one location in the content tree to another:

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

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

using Sitecore.Sandbox.Pipelines.Base;
using Sitecore.Sandbox.Pipelines.Utilities;

namespace Sitecore.Sandbox.Commands
{
    public class CopySubitemsTo : Command
    {
        private const string CopySubitemsPipeline = "uiCopySubitems";
        
        public override void Execute(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            CopySubitems(commandContext);
        }

        private void CopySubitems(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            IEnumerable<Item> subitems = GetSubitems(commandContext);
            StartPipeline(subitems);
        }

        public static IEnumerable<Item> GetSubitems(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            return GetSubitems(commandContext.Items);
        }

        public static IEnumerable<Item> GetSubitems(IEnumerable<Item> items)
        {
            Assert.ArgumentNotNull(items, "items");
            List<Item> list = new List<Item>();

            foreach (Item item in items)
            {
                list.AddRange(item.Children.ToArray());
            }

            return list;
        }

        private void StartPipeline(IEnumerable<Item> subitems)
        {
            Assert.ArgumentNotNull(subitems, "subitems");
            Assert.ArgumentCondition(subitems.Count() > 0, "subitems", "There must be at least one subitem in the collection to copy!");

            IPipelineLauncher pipelineLauncher = CreateNewPipelineLauncher();
            pipelineLauncher.StartPipeline(CopySubitemsPipeline, new CopyItemsArgs(), subitems.First().Database, subitems);
        }

        private IPipelineLauncher CreateNewPipelineLauncher()
        {
            return PipelineLauncher.CreateNewPipelineLauncher(Context.ClientPage);
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            int subitemsCount = CalculateSubitemsCount(commandContext);

            // if this item has no children, let's disable the menu option
            if (subitemsCount < 1)
            {
                return CommandState.Disabled;
            }

            return base.QueryState(commandContext);
        }

        private static int CalculateSubitemsCount(CommandContext commandContext)
        {
            int count = 0;

            foreach (Item item in commandContext.Items)
            {
                count += item.Children.Count;
            }

            return count;
        }
    }
}

My QueryState() method returns CommandState.Disabled if the current context item — although this does offer the ability to have multiple selected items — has no children. More complex logic could be written here, albeit I chose to keep it simple.

Many of the Commands within Sitecore.Shell.Framework.Commands that deal with Sitecore Items use the utility class Sitecore.Shell.Framework.Items. These commands delegate their core Execute() logic to static methods defined in this class.

Instead of creating my own Items utility class, I included my Comamnd’s logic in my Command class instead — I figure doing this makes it easier to illustrate it here (please Mike, stop writing so many classes :)). However, I would argue it should live in its own class.

The Sitecore.Shell.Framework.Commands.Item class also has a private Start() method that launches a pipeline and passes arguments to it. I decided to copy this code into a utility class that does just that, and used it above in my Command. Here is that class and its interface — the PipelineLauncher class:

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

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

namespace Sitecore.Sandbox.Pipelines.Base
{
    public interface IPipelineLauncher
    {
        void StartPipeline(string pipelineName, ClientPipelineArgs args, Database database, IEnumerable<Item> items);
    }
}
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;

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

using Sitecore.Sandbox.Pipelines.Base;

namespace Sitecore.Sandbox.Pipelines.Utilities
{
    public class PipelineLauncher : IPipelineLauncher
    {
        private const char PipeDelimiter = '|';

        private ClientPage ClientPage { get; set; }

        private PipelineLauncher(ClientPage clientPage)
        {
            SetClientPage(clientPage);
        }

        private void SetClientPage(ClientPage clientPage)
        {
            Assert.ArgumentNotNull(clientPage, "clientPage");
            ClientPage = clientPage;
        }

        public void StartPipeline(string pipelineName, ClientPipelineArgs args, Database database, IEnumerable<Item> items)
        {
            Assert.ArgumentNotNullOrEmpty(pipelineName, "pipelineName");
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(database, "database");
            Assert.ArgumentNotNull(items, "items");
            
            ListString listString = new ListString(PipeDelimiter);
            foreach (Item item in items)
            {
                listString.Add(item.ID.ToString());
            }

            NameValueCollection nameValueCollection = new NameValueCollection();
            nameValueCollection.Add("database", database.Name);
            nameValueCollection.Add("items", listString.ToString());

            args.Parameters = nameValueCollection;
            ClientPage.Start(pipelineName, args);
        }

        public static IPipelineLauncher CreateNewPipelineLauncher(ClientPage clientPage)
        {
            return new PipelineLauncher(clientPage);
        }
    }
}

Next, I created a pipeline that does virtually all of the heavy lifting of the command — it is truly the “man behind the curtain”.

Instead of writing all of copying logic to do this from scratch — not a difficult feat to accomplish — I decided to subclass the CopyTo pipeline used by the ‘Copy To’ command. This pipeline already copies multiples items from one location in the content tree to another. The only thing I needed to change was the url of my new dialog (I define a new dialog down below).

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

using Sitecore.Configuration;
using Sitecore.Shell.Framework.Pipelines;

namespace Sitecore.Sandbox.Pipelines.UICopySubitems
{
    public class CopySubitems : CopyItems
    {
        private const string DialogUrlSettingName = "Pipelines.UICopySubitems.CopySubitems.DialogUrl";
        private static readonly string DialogUrl = Settings.GetSetting(DialogUrlSettingName);

        protected override string GetDialogUrl()
        {
            return DialogUrl;
        }
    }
}

This is the definition for my new dialog. This is really just a “copy and paste” job of /sitecore/shell/Applications/Dialogs/CopyTo/CopyTo.xml with some changed copy — I changed ‘Copy Item’ to ‘Copy Subitems’.

This dialog uses the same logic as the ‘Copy Item To’ dialog. I saved this xml into a file named /sitecore/shell/Applications/Dialogs/CopySubitemsTo/CopySubitemsTo.xml.

<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
  <CopyTo>
    <FormDialog Icon="Core3/24x24/Copy_To_Folder.png" Header="Copy Subitems To" 
      Text="Select the location where you want to copy the subitems to." OKButton="Copy">

      <CodeBeside Type="Sitecore.Shell.Applications.Dialogs.CopyTo.CopyToForm,Sitecore.Client"/>

      <DataContext ID="DataContext" Root="/"/>

      <GridPanel Width="100%" Height="100%" Style="table-layout:fixed">
        <Scrollbox Height="100%" Class="scScrollbox scFixSize scFixSize4 scInsetBorder" Background="white" Padding="0px" GridPanel.Height="100%">
          <TreeviewEx ID="Treeview" DataContext="DataContext" Click="SelectTreeNode" ContextMenu='Treeview.GetContextMenu("contextmenu")' />
        </Scrollbox>

        <Border Padding="4px 0px 4px 0px">
          <GridPanel Width="100%" Columns="2">

            <Border Padding="0px 4px 0px 0px">
              <Literal Text="Name:"/>
            </Border>

            <Edit ID="Filename" Width="100%" GridPanel.Width="100%"/>
          </GridPanel>
        </Border>

      </GridPanel>

    </FormDialog>
  </CopyTo>
</control>

Now, we have to wedge in my new pipeline into the Web.config. I did this by defining my new pipeline in a file named /App_Config/Include/CopySubitems.config. I also added a setting for my dialog url — this is being acquired above in my pipeline class. I vehemently loathe hardcoding things :).

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<processors>
			<uiCopySubitems>
				<processor mode="on" type="Sitecore.Sandbox.Pipelines.UICopySubitems.CopySubitems,Sitecore.Sandbox" method="GetDestination"/>
				<processor mode="on" type="Sitecore.Sandbox.Pipelines.UICopySubitems.CopySubitems,Sitecore.Sandbox" method="CheckDestination"/>
				<processor mode="on" type="Sitecore.Sandbox.Pipelines.UICopySubitems.CopySubitems,Sitecore.Sandbox" method="CheckLanguage"/>
				<processor mode="on" type="Sitecore.Sandbox.Pipelines.UICopySubitems.CopySubitems,Sitecore.Sandbox" method="Execute"/>
			</uiCopySubitems>
		</processors>
		<settings>
			<setting name="Pipelines.UICopySubitems.CopySubitems.DialogUrl" value="/sitecore/shell/Applications/Dialogs/Copy Subitems to.aspx" />
		</settings>
	</sitecore>
</configuration>

Next, I had to define my new command in /App_Config/Commands.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

<!-- Some Stuff Defined Here -->

<command name="item:copysubitemsto" type="Sitecore.Sandbox.Commands.CopySubitemsTo, Sitecore.Sandbox" />

<!-- More Stuff Defined Here -->

</configuration>

As I had done in part 1 of this article, I created a new menu item — using the /sitecore/templates/System/Menus/Menu item template. I created a new menu item named ‘Copy Subitems To’ underneath /sitecore/content/Applications/Content Editor/Context Menues/Default/Copying in my Item context menu in the core database:

Copy Subitems To Menu Item

Oh look, I found some subitems I would like to copy somewhere else:

Subitems To Copy

After right-clicking on the parent of my subitems and hovering over ‘Copying’, my beautiful new menu item displays:

'Copy Subitems To Context Menu

I then clicked ‘Copy Subitems To’, and this dialog window appeared. I then selected a destination for my copied subitems, and clicked the ‘Copy’ button:

Copy Subitems To Dialog

And — voilà! — my subitems were copied to my selected destination:

Subitems Copies One Level

I had more fun with this later on by copying nested levels of subitems — it will copy all subitems beyond just one level below.

There are a few moving parts here, but nothing too overwhelming.

I hope you try out building your own custom Item context menu option. Trust me, you will have lots of fun building one. I know I did!

Until next time, happy coding!

Put Things Into Context: Augmenting the Item Context Menu – Part 1

Last week, I asked Lewis Goulden — a friend and former colleague — to name one thing in Sitecore he would like to learn or know more about. His answer was knowing how to add menu options to the Item context menu — particularly the ‘Publish Now’ and ‘Publish Item’ menu options found in the Publish ribbon.

I remembered reading a few blog articles in the past articulating how one would accomplish this. I needed to dig to find any of these articles.

After googling — yes googling is a verb in the English language — a few times, I found an article by John West discussing how to do this and another by Aboo Bolaky. I had also discovered this on pages 259-260 in John West’s book Professional Sitecore Development.

Instead of passing these on to Lewis and wishing him the best of luck in his endeavours augmenting his context menu — definitely not a cool thing to do to a friend — I decided to capture how this is done in this post, and share this with you as well.

Plus, this article sets the cornerstone for part two: augmenting the item context menu using a custom command and pipeline.

Here’s what the context menu looks like unadulterated:

Item Context Menu

My basic goal is to add a new ‘Publishing’ fly-out menu item containing ‘Publish Now’ and ‘Publish Item’ submenu options betwixt the ‘Insert’ and ‘Cut’ menu options.

In other words, I need to go find the ‘Publish Now’ and ‘Publish Item’ menu items in the core database and add these to the Item context menu.

Publishing Ribbon

So, I had to switch over to the core database and fish around to find out the command names of the ‘Publish Now’ and ‘Publish Item’ menu items.

First, I looked at the Publish ribbon (/sitecore/content/Applications/Content Editor/Ribbons/Ribbons/Default/Publish):

Publish Ribbon

The Publish ribbon helped me hone in closer to where I should continue to look to find the publishing menu items.

Next, I went to the Publish strip:

Publish Strip

The Publish strip pretty much tells me I have to keep on digging. Now, I have to go take a look at the Publish chunk.

In the Publish chunk, I finally found the ‘Publish Now’ menu button:

publish now button

I made sure I copied its click field value — we’ll be needing it when we create our own ‘Publish Now’ menu item.

I then navigated to /sitecore/content/Applications/Content Editor/Menues/Publish to snag the command name of the ‘Publish Item’ menu option:

Publish Item

Now, I have all I need to create my own ‘Publishing’ fly-out menu item and its ‘Publish Now’ and ‘Publish Item’ submenu options.

I then went to /sitecore/content/Applications/Content Editor/Context Menues/Default and added a new Item named ‘Divider’ — using the /sitecore/templates/System/Menus/Menu divider template — after the Insert menu option, and created my ‘Publishing’ fly-out menu item using the /sitecore/templates/System/Menus/Menu item template.

Thereafter, I created my ‘Publish Item’ menu item using the same template as its parent ‘Publishing’. I then put the command name for the ‘Publish Item’ menu option found previously into my item’s Message field:

Publish Item Context Menu Button

Next, I created my ‘Publish Now’ menu item using the same template as its parent and sibling. I put in the ‘Publish Now’ menu item’s Message field the command name for the ‘Publish Now’ menu option found during my expedition above:

Publish Now Context Menu Button

Now, let’s take this for a test drive. I switched back to the master database and navigated to my Home node. I then right-clicked:

Publishing Context Menu 1

As you can see, the ‘Publishing’ fly-out menu appears with its publishing submenu options. Trust me, both menu items do work. 🙂

All of this without any code whatsoever!

In part 2 of this article, I will step through building a custom command coupled with a custom pipeline that compose the logic for a custom menu item for the Item context menu that copies all subitems of an ancestor item to a selected destination.

Happy coding and have a Sitecorelicious day! 🙂

Seek and Destroy: Root Out Newlines and Carriage Returns in Multi-Line Text Fields

A couple of days ago, Sitecore MVP Brian Pedersen wrote an article discussing how newlines and carriage returns in Multi-Line Text fields can intrusively launch Sitecore’s “Do you want to save the changes to the item?” dialog box when clicking away from an item — even when you’ve made no changes to the item. Brian then offered an extension method on the String class as a way to remedy this annoyance.

However, Brian’s extension method cannot serve a solution on its own. It has to be invoked from somewhere to stamp out the substring malefactors — newlines (“\n”), carriage returns (“\r”), tabs (“\t”), and non-breaking spaces (“\xA0”).

This article gives one possible solution for uprooting these from Multi-Line Text fields within the Sitecore client by removing them upon item save.

First, I created a utility class that removes specified substrings from a string passed to it.

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

namespace Sitecore.Sandbox.Utilities.StringUtilities.Base
{
    public interface ISubstringAnnihilator
    {
        string AnnihilateSubstrings(string input);
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Sandbox.Utilities.StringUtilities.Base;

namespace Sitecore.Sandbox.Utilities.StringUtilities
{
    public class SubstringAnnihilator : ISubstringAnnihilator
    {
        private const string ReplacementString = " ";
        private static readonly IEnumerable<string> SubstringsToAnnihilate = new string[] { "\r\n", "\n", "\r", "\t", "\xA0"};

        private SubstringAnnihilator()
        {
        }

        public string AnnihilateSubstrings(string input)
        {
            foreach (string substringToAnnihilate in SubstringsToAnnihilate)
            {
                input = input.Replace(substringToAnnihilate, ReplacementString);
            }

            return input;
        }

        public static ISubstringAnnihilator CreateNewSubstringAnnihilator()
        {
            return new SubstringAnnihilator();
        }
    }
}

It would probably be ideal to move the target substrings defined within the SubstringsToAnnihilate string array into a configuration file or even into Sitecore itself. I decided not introduce that complexity here for the sake of brevity.

Next, I created a Save pipeline. I used .NET Reflector to see how other Save pipelines in /configuration/sitecore/processors/saveUI/ in the Web.config were built — I used these as a model for creating my own — and used my SubstringAnnihilator utility class to seek and destroy the target substrings (well, just replace them with a space :)).

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

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

using Sitecore.Sandbox.Utilities.StringUtilities;
using Sitecore.Sandbox.Utilities.StringUtilities.Base;

namespace Sitecore.Sandbox.Pipelines.SaveUI
{
    public class FixMultiLineTextFields
    {
        private static readonly ISubstringAnnihilator Annihilator = SubstringAnnihilator.CreateNewSubstringAnnihilator();

        public void Process(SaveArgs saveArgs)
        {
            FixAllItemFieldsWhereApplicable(saveArgs);
        }

        private static void FixAllItemFieldsWhereApplicable(SaveArgs saveArgs)
        {
            AssertSaveArgs(saveArgs);
            foreach (SaveArgs.SaveItem saveItem in saveArgs.Items)
            {
                FixSaveItemFieldsWhereApplicable(saveItem);
            }
        }

        private static void AssertSaveArgs(SaveArgs saveArgs)
        {
            Assert.ArgumentNotNull(saveArgs, "saveArgs");
            Assert.IsNotNull(saveArgs.Items, "saveArgs.Items");
        }

        private static void FixSaveItemFieldsWhereApplicable(SaveArgs.SaveItem saveItem)
        {
            Item item = GetItem(saveItem);
            foreach (SaveArgs.SaveField saveField in saveItem.Fields)
            {
                FixSaveItemFieldIfApplicable(item, saveField);
            }
        }

        private static Item GetItem(SaveArgs.SaveItem saveItem)
        {
            if (saveItem != null)
            {
                return Client.ContentDatabase.Items[saveItem.ID, saveItem.Language, saveItem.Version];
            }

            return null;
        }

        private static void FixSaveItemFieldIfApplicable(Item item, SaveArgs.SaveField saveField)
        {
            if (ShouldEnsureFieldValue(item, saveField))
            {
                saveField.Value = Annihilator.AnnihilateSubstrings(saveField.Value);
            }
        }

        private static bool ShouldEnsureFieldValue(Item item, SaveArgs.SaveField saveField)
        {
            Field field = item.Fields[saveField.ID];
            return ShouldEnsureFieldValue(field);
        }

        private static bool ShouldEnsureFieldValue(Field field)
        {
            return field.TypeKey == "memo"
                    || field.TypeKey == "multi-line text";
        }
    }
}

Now, it’s time to insert my new Save pipeline within the SaveUI pipeline stack. I’ve done this with my /App_Config/Include/FixMultiLineTextFields.config file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<processors>
			<saveUI>
				<processor mode="on" type="Sitecore.Sandbox.Pipelines.SaveUI.FixMultiLineTextFields, Sitecore.Sandbox" patch:after="processor[@type='Sitecore.Pipelines.Save.TightenRelativeImageLinks, Sitecore.Kernel']" />
			</saveUI>
		</processors>
	</sitecore>
</configuration>

Let’s take the above for a spin. I’ve inserted a sentence with newlines after every word within it into my Blurb Multi-Line Text field:

newlines-galore

Now, I’ve clicked save, and all newlines within my Blurb Multi-Line Text field have been annihilated:

newlines-annihilated

I would like to thank to Brian Pedersen for writing his article the other day — it served as the bedrock for this one, and kindled something in me to write the code above.

Keep smiling when coding!