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

Periodically Unlock Items of Idle Users in Sitecore

$
0
0

In my last post I showed a way to unlock locked items of a user when he/she logs out of Sitecore.

I wrote that article to help out the poster of this thread in one of the forums on SDN.

In response to my reply in that thread — I had linked to my previous post in that reply — John West, Chief Technology Officer of Sitecore, had asked whether we would also want to unlock items of users whose sessions had expired, and another SDN user had alluded to the fact that my solution would not unlock items for users who had closed their browser sessions instead of explicitly logging out of Sitecore.

Immediate after reading these responses, I began thinking about a supplemental solution to unlock items for “idle” users — users who have not triggered any sort of request in Sitecore after a certain amount of time.

I first began tinkering with the idea of using the last activity date/time of the logged in user — this is available as a DateTime in the user’s MembershipUser instance via the LastActivityDate property.

However — after reading this article — I learned this date and time does not mean what I thought it had meant, and decided to search for another way to ascertain whether a user is idle in Sitecore.

After some digging around, I discovered Sitecore.Web.Authentication.DomainAccessGuard.Sessions in Sitecore.Kernel.dll — this appears to be a collection of sessions in Sitecore — and immediately felt elated as if I had just won the lottery. I decided to put it to use in the following class (code in this class will be invoked via a scheduled task in Sitecore):

using System;
using System.Collections.Generic;
using System.Web.Security;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Security.Accounts;
using Sitecore.Tasks;
using Sitecore.Web.Authentication;

namespace Sitecore.Sandbox.Tasks
{
    public class UnlockItemsTask
    {
        private static readonly TimeSpan ElapsedTimeWhenIdle = GetElapsedTimeWhenIdle();

        public void UnlockIdleUserItems(Item[] items, CommandItem command, ScheduleItem schedule)
        {
            if (ElapsedTimeWhenIdle == TimeSpan.Zero)
            {
                return;
            }

            IEnumerable<Item> lockedItems = GetLockedItems(schedule.Database);
            foreach (Item lockedItem in lockedItems)
            {
                UnlockIfApplicable(lockedItem);
            }
        }
        
        private static IEnumerable<Item> GetLockedItems(Database database)
        {
            Assert.ArgumentNotNull(database, "database");
            return database.SelectItems("fast://*[@__lock='%owner=%']");
        }

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

        private static bool ShouldUnlockItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if(!item.Locking.IsLocked())
            {
                return false;
            }

            string owner = item.Locking.GetOwner();
            return !IsUserAdmin(owner) && IsUserIdle(owner);
        }

        private static bool IsUserAdmin(string username)
        {
            Assert.ArgumentNotNullOrEmpty(username, "username");
            User user = User.FromName(username, false);
            Assert.IsNotNull(user, "User must be null due to a wrinkle in the interwebs :-/");
            return user.IsAdministrator;
        }

        private static bool IsUserIdle(string username)
        {
            Assert.ArgumentNotNullOrEmpty(username, "username");
            DomainAccessGuard.Session userSession = DomainAccessGuard.Sessions.Find(session => session.UserName == username);
            if(userSession == null)
            {
                return true;
            }

            return userSession.LastRequest.Add(ElapsedTimeWhenIdle) <= DateTime.Now;
        }

        private void Unlock(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            try
            {
                string owner = item.Locking.GetOwner();
                item.Editing.BeginEdit();
                item.Locking.Unlock();
                item.Editing.EndEdit();
                Log.Info(string.Format("Unlocked {0} - was locked by {1}", item.Paths.Path, owner), this);
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }
        }

        private static TimeSpan GetElapsedTimeWhenIdle()
        {
            TimeSpan elapsedTimeWhenIdle;
            if (TimeSpan.TryParse(Settings.GetSetting("UnlockItems.ElapsedTimeWhenIdle"), out elapsedTimeWhenIdle))
            {
                return elapsedTimeWhenIdle;
            }
            
            return TimeSpan.Zero;
        }
    }
}

Methods in the class above grab all locked items in Sitecore via a fast query, and unlock them if the users of each are not administrators, and are idle — I determine this from an idle threshold value that is stored in a custom setting (see the patch configuration file below) and the last time the user had made any sort of request in Sitecore via his/her DomainAccessGuard.Session instance from Sitecore.Web.Authentication.DomainAccessGuard.Sessions.

If a DomainAccessGuard.Session instance does not exist for the user — it’s null — this means the user’s session had expired, so we should also unlock the item.

I’ve also included code to log which items have been unlocked by the Unlock method — for auditing purposes — and of course log exceptions if any are encountered — we must do all we can to support our support teams by capturing information in log files :) .

I then created a patch configuration file to store our idle threshold value — I’ve used one minute here for testing (I can’t sit around all day waiting for items to unlock ;) ):

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <setting name="UnlockItems.ElapsedTimeWhenIdle" value="00:00:01:00" />
    </settings>
  </sitecore>
</configuration> 

I then created a task command for the class above in Sitecore:

unlock-items-task-command

I then mapped the task command to a scheduled task (to learn about scheduled tasks, see John West’s blog post where he discusses them):

unlock-items-scheduled-task

Let’s light the fuse on this, and see what it does.

I logged into Sitecore using one of my test accounts, and locked some items:

locked-some-items

I then logged into Sitecore using a different account in a different browser session, and navigated to one of the locked items:

mike-locked-items

I then walked away, made a cup of coffee, returned, and saw this:

items-unlocked

I opened up my latest Sitecore log, and saw the following:

unlocked-log

I do want to caution you from running off with this code, and putting it into your Sitecore instance(s) — it is an all or nothing solution (it will unlock items for all non-adminstrators which might invoke some anger in users, and also defeat the purpose of locking items in the first place), so it’s quite important that a business decision is made before using this solution, or one that is similar in nature.

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



Restrict Certain Types of Files From Being Uploaded in Sitecore

$
0
0

Tonight I was struck by a thought while conducting research for a future blog post: should we prevent users from uploading certain types of files in Sitecore for security purposes?

You might thinking “Prevent users from uploading files? Mike, what on Earth are you talking about?”

What I’m suggesting is we might want to consider restricting certain types of files — specifically executables (these have an extension of .exe) and DOS command files (these have an extension of .com) — from being uploaded into Sitecore, especially when files can be easily downloaded from the media library.

Why should we do this?

Doing this will curtail the probability of viruses being spread among our Sitecore users — such could happen if one user uploads an executable that harbors a virus, and other users of our Sitecore system download that tainted executable, and run it on their machines.

As a “proof of concept”, I built the following uiUpload pipeline processor — the uiUpload pipeline lives in /configuration/sitecore/processors/uiUpload in your Sitecore instance’s Web.config — to restrict certain types of files from being uploaded into Sitecore:

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

using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Pipelines.Upload;

namespace Sitecore.Sandbox.Pipelines.Upload
{
    public class CheckForRestrictedFiles : UploadProcessor
    {
        private List<string> _RestrictedExtensions;
        private List<string> RestrictedExtensions
        {
            get
            {
                if (_RestrictedExtensions == null)
                {
                    _RestrictedExtensions = new List<string>();
                }

                return _RestrictedExtensions;
            }
        }

        public void Process(UploadArgs args)
        {
            foreach(string fileKey in args.Files)
            {
                string fileName = GetFileName(args.Files, fileKey);
                string extension = Path.GetExtension(fileName);
                if (IsRestrictedExtension(extension))
                {
                    args.ErrorText = Translate.Text(string.Format("The file \"{0}\" cannot be uploaded. Files with an extension of {1} are not allowed.", fileName, extension));
                    Log.Warn(args.ErrorText, this);
                    args.AbortPipeline();
                }
            }
        }

        private static string GetFileName(HttpFileCollection files, string fileKey)
        {
            Assert.ArgumentNotNull(files, "files");
            Assert.ArgumentNotNullOrEmpty(fileKey, "fileKey");
            return files[fileKey].FileName;
        }

        private bool IsRestrictedExtension(string extension)
        {
            return RestrictedExtensions.Exists(restrictedExtension => string.Equals(restrictedExtension, extension, StringComparison.CurrentCultureIgnoreCase));
        }

        protected virtual void AddRestrictedExtension(XmlNode configNode)
        {
            if (configNode == null || string.IsNullOrWhiteSpace(configNode.InnerText))
            {
                return;
            }

            RestrictedExtensions.Add(configNode.InnerText);
        }
    }
}

The class above ascertains whether each uploaded file has an extension that is restricted — restricted extensions are defined in the configuration file below, and are added to a list of strings via the AddRestrictedExtensions method — and logs an error message when a file possessing a restricted extension is encountered.

I then tied everything together using the following patch configuration file including specifying some file extensions to restrict:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <uiUpload>
        <processor mode="on" type="Sitecore.Sandbox.Pipelines.Upload.CheckForRestrictedFiles, Sitecore.Sandbox" patch:before="processor[@type='Sitecore.Pipelines.Upload.CheckSize, Sitecore.Kernel']">
          <restrictedExtensions hint="raw:AddRestrictedExtension">
            <!-- Be sure to prefix with a dot -->
            <extension>.exe</extension>
            <extension>.com</extension>
          </restrictedExtensions>
        </processor>
      </uiUpload>
    </processors>
  </sitecore>
</configuration>

Let’s try this out.

I went to the media library, and attempted to upload an executable:

upload-exe-dialog_001

After clicking the “Open” button, I was presented with the following:

upload-exe-error

An error in my Sitecore instance’s latest log file conveys why I could not upload the chosen file:

upload-exe-error-log

If you have thoughts on this, or have ideas for other processors that should be added to uiUpload pipeline, please share in a comment.


Specify Which Sitecore Web Forms for Marketers Form To Render Via the Query String

$
0
0

Today I saw a post in one of the SDN forums asking how one could go about building a page in Sitecore that can render a Web Forms for Marketers (WFFM) form based on an ID passed via the query string.

I built the following WFFM FormRenderer as a “proof of concept” to accomplish this — this solution assumes the ID we are passing is the ID of the form, and not some other ID (or guid):

using System;
using System.Web;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Form.Core.Configuration;
using Sitecore.Form.Core.Renderings;

namespace Sitecore.Sandbox.Form.Core.Renderings
{
    public class DetectIDFormRenderer : FormRender
    {
        protected override void OnInit(System.EventArgs e)
        {
            string detectedFormId = GetDetectedFormId();
            if (IsValidFormId(detectedFormId))
            {
                FormID = detectedFormId;
            }
            
            base.OnInit(e);
        }

        private static string GetDetectedFormId()
        {
            return HttpContext.Current.Request["formId"];
        }

        private static bool IsValidFormId(string id)
        {
            return !string.IsNullOrWhiteSpace(id) 
                    && IsID(id) 
                    && IsFormId(id);
        }
        
        private static bool IsID(string id)
        {
            Sitecore.Data.ID sitecoreID;
            return Sitecore.Data.ID.TryParse(id, out sitecoreID);
        }

        private static bool IsFormId(string id)
        {
            Item item = StaticSettings.ContextDatabase.GetItem(id);
            return item != null && item.TemplateID == IDs.FormTemplateID;
        }
    }
}

The FormRenderer above grabs the specified form’s ID via a query string parameter, ascertains whether it’s truly an ID, and determines whether it is an ID of a WFFM Form in Sitecore — these are done via the IsID and IsFormId methods.

If the supplied form ID is valid, we save it to the FormID property defined in the base FormerRender class. Otherwise, we flow through to the “out of the box” logic.

Now it’s time to register the above class in Sitecore.

I duplicated the “out of the box” Form Webcontrol under /sitecore/layout/Renderings/Modules/Web Forms for Marketers, renamed the item to something appropriate, and updated the code-defining fields to point to our new FormRender above:

Detect-ID-Form-FormRenderer

I decided to reuse an existing page item with a WFFM form — I didn’t want to step through ‘Insert Form’ wizard so that I could save time — and swapped out the “out of the box” Form Webcontrol with the new one we created above:

webcontrol-switch

I ensured we had a default form set just in case of query string manipulation, or in the event the form cannot be found by the given ID:

default-form-is-set

I published everything, and navigated to my form page:

no-form-specified

I then specified the empty guid:

invalid-form-specified

I manipulated the query string again, but this time passing a valid form ID:

valid-form-specified

I then changed the form ID again but with another valid form ID:

another-valid-form-specified

If you have any suggestions around making this better, or ideas for a different solution, please drop a comment.


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

$
0
0

Last week someone started a thread in one of the SDN forums asking how one could go about making Sitecore resize images larger than a specific width down to that width.

Yesterday an astute SDN visitor recommended using a custom getMediaStream pipeline processor to set the maximum width for images — a property for this maximum width is exposed via the GetMediaStreamPipelineArgs parameters object passed through the getMediaStream pipeline.

I thought I would try out the suggestion, and came up with the following getMediaStream pipeline processor:

using Sitecore.Diagnostics;

using Sitecore.Resources.Media;

namespace Sitecore.Sandbox.Resources.Media
{
    public class MaxWidthProcessor
    {
        public int MaxWidth { get; set; }

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

            args.Options.MaxWidth = MaxWidth;
        }

        private bool ShouldSetMaxWidth(GetMediaStreamPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            return MaxWidth > 0 && args.Options.MaxWidth < 1;
        }
    }
}

I then interlaced it into the getMediaStream pipeline before the ResizeProcessor processor — this is the processor where the magical resizing happens — using the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getMediaStream>
        <processor patch:before="processor[@type='Sitecore.Resources.Media.ResizeProcessor, Sitecore.Kernel']"
                   type="Sitecore.Sandbox.Resources.Media.MaxWidthProcessor, Sitecore.Sandbox">
          <MaxWidth>1024</MaxWidth>
        </processor>  
      </getMediaStream>
    </pipelines>
  </sitecore>
</configuration>

The maximum width for images is set to 1024 pixels — the width I am using in my test below.

Let’s see how we did.

I decided to use one of my favorite images that ships with Sitecore for testing:

lighthouse-small

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

lighthouse-properties

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

resized-during-upload

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


Insert a New Item After a Sibling Item in Sitecore

$
0
0

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

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

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

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

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

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

        private string ModalIcon { get; set; }

        private string ModalTitle { get; set; }

        private string ModalInstructions { get; set; }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            return ordering;
        }

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

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

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

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

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

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

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

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

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

Let’s test this out.

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

no-new-items

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

item-context-menu-insert-from-template

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

insert-from-template-dialog

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

selected-insert-after

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

was-inserted-after

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


Resolve Media Library Items Linked in Sitecore Aliases

$
0
0

Tonight I was doing research on extending the aliases feature in Sitecore, and discovered media library items linked in them are not served correctly “out of the box”:

pizza-alias-no-workie

As an enhancement, I wrote the following HttpRequestProcessor subclass to be used in the httpRequestBegin pipeline:

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.HttpRequest;
using Sitecore.Resources.Media;
using Sitecore.Web;

namespace Sitecore.Sandbox.Pipelines.HttpRequest
{
    public class MediaAliasResolver : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!CanProcessAliases())
            {
                return;
            }

            string mediaUrl = GetMediaAliasTargetUrl(args);
            if (string.IsNullOrWhiteSpace(mediaUrl))
            {
                return;
            }

            Context.Page.FilePath = mediaUrl;
        }

        private static bool CanProcessAliases()
        {
            return Settings.AliasesActive && Context.Database != null;
        }

        private static string GetMediaAliasTargetUrl(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            ID targetID = Context.Database.Aliases.GetTargetID(args.LocalPath);
            if (targetID.IsNull)
            {
                return string.Empty;
            }

            Item targetItem = args.GetItem(targetID);
            if (targetItem == null || !IsMediaItem(targetItem))
            {
                return string.Empty;
            }

            return GetAbsoluteMediaUrl(targetItem);
        }

        private static bool IsMediaItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item.Paths.IsMediaItem && item.TemplateID != TemplateIDs.MediaFolder;
        }

        private static string GetAbsoluteMediaUrl(MediaItem mediaItem)
        {
            string relativeUrl = MediaManager.GetMediaUrl(mediaItem);
            return WebUtil.GetFullUrl(relativeUrl);
        }
    }
}

The HttpRequestProcessor subclass above — after ascertaining the aliases feature is turned on, and the item linked in the requested alias is a media library item — gets the absolute URL for the media library item, and sets it on the FilePath property of the Sitecore.Context.Page instance — this is exactly how the “out of the box” Sitecore.Pipelines.HttpRequest.AliasResolver handles external URLs — and passes along the HttpRequestArgs instance.

I then wedged the HttpRequestProcessor subclass above into the httpRequestBegin pipeline directly before the Sitecore.Pipelines.HttpRequest.AliasResolver:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.AliasResolver, Sitecore.Kernel']"
                   type="Sitecore.Sandbox.Pipelines.HttpRequest.MediaAliasResolver, Sitecore.Sandbox" />
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

Let’s take this for a spin.

I had already defined the following alias in Sitecore beforehand — the error page at the top of this post is evidence of that:

pizza-alias

After navigating to http://sandbox/pizza — the URL to the pizza alias in my local Sitecore sandbox instance (don’t click on this link because it won’t go anywhere unless you have a website named sandbox running on your local machine) — I was brought to the media library image on the front-end:

media-alias-pizza-redirected

If you have any recommendations on improving this, or further thoughts on using aliases in Sitecore, please share in a comment below.

Until next time, have a pizzalicious day!


Choose Template Fields to Display in the Sitecore Content Editor

$
0
0

The other day I was going through search terms people had used to get to my blog, and discovered a few people made their way to my blog by searching for ‘sitecore hide sections in data template’.

I had built something like this in the past, but no longer remember how I had implemented that particular solution — not that I could show you that solution since it’s owned by a previous employer — and decided I would build another solution to accomplish this.

Before I move forward, I would like to point out that Sitecore MVP Andy Uzick wrote a blog post recently showing how to hide fields and sections in the content editor, though I did not have much luck with hiding sections in the way that he had done it — sections with no fields were still displaying for me in the content editor — and I decided to build a different solution altogether to make this work.

I thought it would be a good idea to let users turn this functionality on and off via a checkbox in the ribbon, and used some code from a previous post — in this post I had build an object to keep track of the state of a checkbox in the ribbon — as a model. In the spirit of that object, I defined the following interface:

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public interface IRegistrySettingToggle
    {
        bool IsOn();

        void TurnOn();

        void TurnOff();
    }
}

I then created the following abstract class which implements the interface above, and stores the state of the setting defined by the given key — the key of the setting and the “on” value are passed to it via a subclass — in the Sitecore registry.

using Sitecore.Diagnostics;

using Sitecore.Web.UI.HtmlControls;

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public abstract class RegistrySettingToggle : IRegistrySettingToggle
    {
        private string RegistrySettingKey { get; set; }

        private string RegistrySettingOnValue { get; set; }

        protected RegistrySettingToggle(string registrySettingKey, string registrySettingOnValue)
        {
            SetRegistrySettingKey(registrySettingKey);
            SetRegistrySettingOnValue(registrySettingOnValue);
        }

        private void SetRegistrySettingKey(string registrySettingKey)
        {
            Assert.ArgumentNotNullOrEmpty(registrySettingKey, "registrySettingKey");
            RegistrySettingKey = registrySettingKey;
        }

        private void SetRegistrySettingOnValue(string registrySettingOnValue)
        {
            Assert.ArgumentNotNullOrEmpty(registrySettingOnValue, "registrySettingOnValue");
            RegistrySettingOnValue = registrySettingOnValue;
        }

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

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

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

I then built the following class to toggle the display settings for our displayable fields:

using System;

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public class ShowDisplayableFieldsOnly : RegistrySettingToggle
    {
        private const string RegistrySettingKey = "/Current_User/Content Editor/Show Displayable Fields Only";
        private const string RegistrySettingOnValue = "on";

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

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

                return current;
            }
        }

        private ShowDisplayableFieldsOnly()
            : base(RegistrySettingKey, RegistrySettingOnValue)
        {
        }
    }
}

It passes its Sitecore registry key and “on” state value to the RegistrySettingToggle base class, and employs the Singleton pattern — I saw no reason for there to be multiple instances of this object floating around.

In order to use a checkbox in the ribbon, we have to create a new command for it:

using System.Linq;

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

using Sitecore.Sandbox.Utilities.ClientSettings;

namespace Sitecore.Sandbox.Commands
{
    public class ToggleDisplayableFieldsVisibility : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            ToggleDisplayableFields();
            Refresh(context);
        }

        private static void ToggleDisplayableFields()
        {
            IRegistrySettingToggle showDisplayableFieldsOnly = ShowDisplayableFieldsOnly.Current;
            if (!showDisplayableFieldsOnly.IsOn())
            {
                showDisplayableFieldsOnly.TurnOn();
            }
            else
            {
                showDisplayableFieldsOnly.TurnOff();
            }
        }

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

            return CommandState.Down;
        }

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

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

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

The command above leverages the instance of the ShowDisplayableFieldsOnly class defined above to turn the displayable fields feature on and off.

I followed the creation of the command above with the definition of the ribbon checkbox in the core database:

show-displayable-fields-checkbox-defined

The command name above — which is set in the Click field — is defined in the patch configuration file towards the end of this post.

I then created the following data template with a TreelistEx field to store the displayable fields:

displayable-fields-template

The TreelistEx field above will pull in all sections and their fields into the TreelistEx dialog, but only allow the selection of template fields, as is dictated by the following parameters that I have mapped in its Source field:

DataSource=/sitecore/templates/Sample/Sample Item&IncludeTemplatesForSelection=Template field&IncludeTemplatesForDisplay=Template section,Template field&AllowMultipleSelection=no

I then set this as a base template in my sandbox solution’s Sample Item template:

set-displayable-fields-template-as-base

In order to remove fields, we need a <getContentEditorFields> pipeline processor. I built the following class for to serve as one:

using System;

using Sitecore.Configuration;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Data.Templates;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentManager;
using Sitecore.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields;

using Sitecore.Sandbox.Utilities.ClientSettings;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields
{
    public class RemoveUndisplayableFields
    {
        public void Process(GetContentEditorFieldsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Item, "args.Item");
            Assert.ArgumentCondition(!string.IsNullOrWhiteSpace(DisplayableFieldsFieldName), "DisplayableFieldsFieldName", "DisplayableFieldsFieldName must be set in the configuration file!");
            if (!ShowDisplayableFieldsOnly.Current.IsOn())
            {
                return;
            }

            foreach (Editor.Section section in args.Sections)
            {
                AddDisplayableFields(args.Item[DisplayableFieldsFieldName], section);
            }
        }

        private void AddDisplayableFields(string displayableFieldIds, Editor.Section section)
        {
            Editor.Fields displayableFields = new Editor.Fields();
            foreach (Editor.Field field in section.Fields)
            {
                if (IsDisplayableField(displayableFieldIds, field))
                {
                    displayableFields.Add(field);
                }
            }

            section.Fields.Clear();
            section.Fields.AddRange(displayableFields);
        }

        private bool IsDisplayableField(string displayableFieldIds, Editor.Field field)
        {
            if (IsStandardValues(field.ItemField.Item))
            {
                return true;
            }

            if (IsDisplayableFieldsField(field.ItemField))
            {
                return false;
            }

            return IsStandardTemplateField(field.ItemField)
                    || string.IsNullOrWhiteSpace(displayableFieldIds)
                    || displayableFieldIds.Contains(field.ItemField.ID.ToString());
        }
        
        private bool IsDisplayableFieldsField(Field field)
        {
            return string.Equals(field.Name, DisplayableFieldsFieldName, StringComparison.CurrentCultureIgnoreCase);
        }

        private static bool IsStandardValues(Item item)
        {
            if (item.Template.StandardValues != null)
            {
                return item.Template.StandardValues.ID == item.ID;
            }

            return false;
        }

        private bool IsStandardTemplateField(Field field)
        {
            Assert.IsNotNull(StandardTemplate, "The Stardard Template could not be found.");
            return StandardTemplate.ContainsField(field.ID);
        }

        private static Template GetStandardTemplate()
        {
            return TemplateManager.GetTemplate(Settings.DefaultBaseTemplate, Context.ContentDatabase);
        }

        private Template _StandardTemplate;
        private Template StandardTemplate
        {
            get
            {
                if (_StandardTemplate == null)
                {
                    _StandardTemplate = GetStandardTemplate();
                }

                return _StandardTemplate;
            }
        }

        private string DisplayableFieldsFieldName { get; set; }
    }
}

The class above iterates over all fields for the supplied item, and adds only those that were selected in the Displayable Fields TreelistEx field, and also Standard Fields — we don’t want to remove these since they are shown/hidden by the Standard Fields feature in Sitecore — to a new Editor.Fields collection. This new collection is then set on the GetContentEditorFieldsArgs instance.

Plus, we don’t want to show the Displayable Fields TreelistEx field when the feature is turned on, and we are on an item. This field should only display when we are on the standard values item when the feature is turned on — this is how we will choose our displayable fields.

Now we have to handle sections without fields — especially after ripping them out via the pipeline processor above.

I built the following class to serve as a <renderContentEditor> pipeline processor to do this:

using System.Linq;

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

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor
{
    public class FilterSectionsWithFields
    {
        public void Process(RenderContentEditorArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.Sections = GetSectionsWithFields(args.Sections);
        }

        private static Editor.Sections GetSectionsWithFields(Editor.Sections sections)
        {
            Assert.ArgumentNotNull(sections, "sections");
            Editor.Sections sectionsWithFields = new Editor.Sections();
            foreach (Editor.Section section in sections)
            {
                AddIfContainsFields(sectionsWithFields, section);
            }

            return sectionsWithFields;
        }

        private static void AddIfContainsFields(Editor.Sections sections, Editor.Section section)
        {
            Assert.ArgumentNotNull(sections, "sections");
            Assert.ArgumentNotNull(section, "section");
            if (!ContainsFields(section))
            {
                return;
            }

            sections.Add(section);
        }

        private static bool ContainsFields(Editor.Section section)
        {
            Assert.ArgumentNotNull(section, "section");
            return section.Fields != null && section.Fields.Any();
        }
    }
}

It basically builds a new collection of sections that contain at least one field, and sets it on the RenderContentEditorArgs instance being passed through the <renderContentEditor> pipeline.

In order for this to work, we must make this pipeline processor run before all other processors.

I tied all of the above together with the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="contenteditor:ToggleDisplayableFieldsVisibility" type="Sitecore.Sandbox.Commands.ToggleDisplayableFieldsVisibility, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <getContentEditorFields>
        <processor type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields.RemoveUndisplayableFields, Sitecore.Sandbox">
          <DisplayableFieldsFieldName>Displayable Fields</DisplayableFieldsFieldName>
        </processor>  
      </getContentEditorFields>
      <renderContentEditor>
        <processor patch:before="processor[@type='Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderSkinedContentEditor, Sitecore.Client']"
                   type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.FilterSectionsWithFields, Sitecore.Sandbox"/>
      </renderContentEditor>
    </pipelines>
  </sitecore>
</configuration>

Let’s try this out.

I navigated to the standard values item for my Sample Item data template, and selected the fields I want to display in the content editor:

selected-some-fields

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

displayable-fields-on

As you can see, only the fields we had chosen display — along with standard fields since the Standard Fields checkbox is checked.

I then turned off the displayable fields feature:

displayable-fields-off

Now all fields for the item display.

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

standard-fields-off-displayable-fields-on

Now only our selected fields display.

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


Perform a Virus Scan on Files Uploaded into Sitecore

$
0
0

Last week I took notice of a SDN forum post asking whether there are any best practices for checking files uploaded into Sitecore for viruses.

Although I am unaware of there being any best practices for this, adding a processor into the <uiUpload> pipeline to perform virus scans has been on my plate for the past month. Not being able to find an open source .NET antivirus library has been my major blocker for building one.

However, after many hours of searching the web — yes, I do admit some of this time is sprinkled with idle surfing — over multiple weeks, I finally discovered nClam — a .NET client library for scanning files using a Clam Antivirus server (I setup one using instructions enumerated here).

Before I continue, I would like to caution you on using this or any antivirus solution before doing a substantial amount of research — I do not know how robust the solution I am sharing actually is, and I am by no means an antivirus expert. The purpose of this post is to show how one might go about adding antivirus capabilities into Sitecore, and I am not advocating or recommending any particular antivirus software package. Please consult with an antivirus expert before using any antivirus software/.NET client library .

The solution I came up with uses the adapter design pattern — it basically wraps and makes calls to the nClam library, and is accessible through the following antivirus scanner interface:

using System.IO;

namespace Sitecore.Sandbox.Security.Antivirus
{
    public interface IScanner
    {
        ScanResult Scan(Stream sourceStream);

        ScanResult Scan(string filePath);
    }
}

This interface defines the bare minimum methods our antivirus classes should have. Instances of these classes should offer the ability to scan a file through the file’s Stream, or scan a file located on the server via the supplied file path.

The two scan methods defined by the interface must return an instance of the following data transfer object:

namespace Sitecore.Sandbox.Security.Antivirus
{
    public class ScanResult
    {
        public string Message { get; set; }

        public ScanResultType ResultType { get; set; }
    }
}

Instances of the above class contain a detailed message coupled with a ScanResultType enumeration value which conveys whether the scanned file is clean, contains a virus, or something else went wrong during the scanning process:

namespace Sitecore.Sandbox.Security.Antivirus
{
    public enum ScanResultType
    {
        Clean,
        VirusDetected,
        Error,
        Unknown
    }
}

I used the ClamScanResults enumeration as a model for the above.

I created and used the ScanResultType enumeration instead of the ClamScanResults enumeration so that this solution can be extended for other antivirus libraries — or calls could be made to other antivirus software through the command-line — and these shouldn’t be tightly coupled to the nClam library.

I then wrapped the nClam library calls in the following ClamScanner class:

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

using Sitecore.Diagnostics;

using nClam;

namespace Sitecore.Sandbox.Security.Antivirus
{
    public class ClamScanner : IScanner
    {
        private ClamClient Client { get; set; }

        public ClamScanner(string server, string port)
            : this(server, int.Parse(port))
        {
        }

        public ClamScanner(string server, int port)
            : this(new ClamClient(server, port))
        {
        }

        public ClamScanner(ClamClient client)
        {
            SetClient(client);
        }

        private void SetClient(ClamClient client)
        {
            Assert.ArgumentNotNull(client, "client");
            Client = client;
        }

        public ScanResult Scan(Stream sourceStream)
        {
            ScanResult result;
            try
            {
                result = CreateNewScanResult(Client.SendAndScanFile(sourceStream));
            }
            catch (Exception ex)
            {
                result = CreateNewExceptionScanResult(ex);
            }

            return result;
        }

        public ScanResult Scan(string filePath)
        {
            ScanResult result;
            try
            {
                result = CreateNewScanResult(Client.SendAndScanFile(filePath));
            }
            catch (Exception ex)
            {
                result = CreateNewExceptionScanResult(ex);
            }

            return result;
        }

        private static ScanResult CreateNewScanResult(ClamScanResult result)
        {
            Assert.ArgumentNotNull(result, "result");
            if (result.Result == ClamScanResults.Clean)
            {
                return CreateNewScanResult("Yay! No Virus found!", ScanResultType.Clean);
            }

            if (result.Result == ClamScanResults.VirusDetected)
            {
                string message = string.Format("Oh no! The {0} virus was found!", result.InfectedFiles.First().VirusName);
                return CreateNewScanResult(message, ScanResultType.VirusDetected);
            }

            if (result.Result == ClamScanResults.Error)
            {
                string message = string.Format("Something went terribly wrong somewhere. Details: {0}", result.RawResult);
                return CreateNewScanResult(message, ScanResultType.Error);
            }

            return CreateNewScanResult("I have no clue about what just happened.", ScanResultType.Unknown);
        }

        private static ScanResult CreateNewExceptionScanResult(Exception ex)
        {
            string message = string.Format("Something went terribly wrong somewhere. Details:\n{0}", ex.ToString());
            return CreateNewScanResult(ex.ToString(), ScanResultType.Error);
        }

        private static ScanResult CreateNewScanResult(string message, ScanResultType type)
        {
            return new ScanResult
            {
                Message = message,
                ResultType = type
            };
        }
    }
}

Methods in the above class make calls to the nClam library, and return a ScanResult intance containing detailed information about the file scan.

Next I developed the following <uiUpload> pipeline processor to use instances of classes that define the IScanner interface above, and these instances are set via Sitecore when instantiating this pipeline processor — the ClamScanner type is defined in the patch configuration file shown later in this post:

using System.IO;
using System.Web;

using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Pipelines.Upload;

using Sitecore.Sandbox.Security.Antivirus;

namespace Sitecore.Sandbox.Pipelines.Upload
{
    public class ScanForViruses
    {
        public void Process(UploadArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            foreach (string fileKey in args.Files)
            {
                HttpPostedFile file = args.Files[fileKey];
                ScanResult result = ScanFile(file.InputStream);
                if(result.ResultType != ScanResultType.Clean)
                {
                    args.ErrorText = Translate.Text(string.Format("The file \"{0}\" cannot be uploaded. Reason: {1}", file.FileName, result.Message));
                    Log.Warn(args.ErrorText, this);
                    args.AbortPipeline();
                }
            }
        }

        private ScanResult ScanFile(Stream fileStream)
        {
            Assert.ArgumentNotNull(fileStream, "fileStream");
            return Scanner.Scan(fileStream);
        }

        private IScanner Scanner { get; set; }
    }
}

Uploaded files are passed to the IScanner instance, and the pipeline is aborted if something isn’t quite right — this occurs when a virus is detected, or an error is reported by the IScanner instance. If a virus is discovered, or an error occurs, the message contained within the ScanResult instance is captured in the Sitecore log.

I then glued everything together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <uiUpload>
        <processor mode="on" patch:before="processor[@type='Sitecore.Pipelines.Upload.CheckSize, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.Upload.ScanForViruses, Sitecore.Sandbox">
          <Scanner type="Sitecore.Sandbox.Security.Antivirus.ClamScanner">
            <param desc="server">localhost</param>
            <param desc="port">3310</param>
          </Scanner>
        </processor>
      </uiUpload>
    </processors>
  </sitecore>
</configuration>

I wish I could show you this in action when a virus is discovered in an uploaded file. However, I cannot put my system at risk by testing with an infected file.

But, I can show you what happens when an error is detected. I will do this by shutting down the Clam Antivirus server on my machine:

scanner-turned-off

On upload, I see the following:

tried-to-upload-file

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

log-error

Let’s turn it back on:

scanner-turned-on

I can now upload files again:

upload-no-error

If you have any suggestions on making this better, or know of a way to test it with a virus, please share in a comment below.



Restart the Sitecore Client and Server Using Custom Pipelines

$
0
0

Last week Michael West asked me about creating shortcuts to restart the Sitecore client and server via this tweet, and I was immediately curious myself on what was needed to accomplish this.

If you are unfamiliar with restarting the Sitecore client and server, these are options that are presented to Sitecore users — typically developers — after installing a package into Sitecore:

install-package-end-wizard

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

restart-code-in-InstallPackageForm

Michael West had conveyed how he wished these two lines of code lived in pipelines, and that prompted me to write this article — basically encapsulate the logic above into two custom pipelines: one to restart the Sitecore client, and the other to restart the Sitecore server.

I decided to define the concept of a ‘Restarter’, an object that restarts something — this could be anything — and defined the following interface for such objects:

namespace Sitecore.Sandbox.Utilities.Restarters
{
    public interface IRestarter
    {
        void Restart();
    }
}

I then created the following IRestarter for the Sitecore client:

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Utilities.Restarters
{
    public class SitecoreClientRestarter : IRestarter
    {
        private ClientResponse ClientResponse { get; set; }

        public SitecoreClientRestarter()
            : this(Context.ClientPage)
        {
        }

        public SitecoreClientRestarter(ClientPage clientPage)
        {
            SetClientResponse(clientPage);
        }

        public SitecoreClientRestarter(ClientResponse clientResponse)
        {
            SetClientResponse(clientResponse);
        }

        private void SetClientResponse(ClientPage clientPage)
        {
            Assert.ArgumentNotNull(clientPage, "clientPage");
            SetClientResponse(clientPage.ClientResponse);
        }

        private void SetClientResponse(ClientResponse clientResponse)
        {
            Assert.ArgumentNotNull(clientResponse, "clientResponse");
            ClientResponse = clientResponse;
        }

        public void Restart()
        {
            ClientResponse.Broadcast(ClientResponse.SetLocation(string.Empty), "Shell");
        }
    }
}

The class above has three constructors. One constructor takes an instance of Sitecore.Web.UI.Sheer.ClientResponse — this lives in Sitecore.Kernel.dll — and another constructor takes in an instance of
Sitecore.Web.UI.Sheer.ClientPage — this also lives in Sitecore.Kernel.dll — which contains a property instance of Sitecore.Web.UI.Sheer.ClientResponse, and this instance is set on the ClientResponse property of the SitecoreClientRestarter class.

The third constructor — which is parameterless — calls the constructor that takes in a Sitecore.Web.UI.Sheer.ClientPage instance, and passes the ClientResponse instance set in Sitecore.Context.

I followed building the above class with another IRestarter — one that restarts the Sitecore server:

using Sitecore.Install;

namespace Sitecore.Sandbox.Utilities.Restarters
{
    public class SitecoreServerRestarter : IRestarter
    {
        public SitecoreServerRestarter()
        {
        }

        public void Restart()
        {
            Installer.RestartServer();
        }
    }
}

There really isn’t much happening in the class above. It just calls the static method RestartServer() — this method changes the timestamp on the Sitecore instance’s Web.config to trigger a web application restart — on Sitecore.Install.Installer in Sitecore.Kernel.dll.

Now we need to a way to use the IRestarter classes above. I built the following class to serve as a processor of custom pipelines I define later on in this post:

using Sitecore.Diagnostics;
using Sitecore.Pipelines;

using Sitecore.Sandbox.Utilities.Restarters;

namespace Sitecore.Sandbox.Pipelines.RestartRestarter
{
    public class RestartRestarterOperations
    {
        private IRestarter Restarter { get; set; }

        public void Process(PipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(Restarter, "Restarter");
            Restarter.Restart();
        }
    }
}

Through a pipeline processor configuration setting, we define the type of IRestarter — it’s magically created by Sitecore when the pipeline processor instance is created.

After some null checks, the Process() method invokes Restart() on the IRestarter instance, ultimately restarting whatever the IRestarter is set to restart.

I then needed a way to test the pipelines I define later on in this post. I built the following class to serve as commands that I added into the Sitecore ribbon:

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

namespace Sitecore.Sandbox.Commands.Admin
{
    public class Restart : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Parameters, "context.Parameters");
            Assert.ArgumentNotNullOrEmpty(context.Parameters["pipeline"], "context.Parameters[\"pipeline\"]");
            CorePipeline.Run(context.Parameters["pipeline"], new PipelineArgs());
        }
    }
}

The command above expects a pipeline name to be supplied via the Parameters NameValueCollection instance set on the CommandContext instance passed to the Execute method() — I show later on in this post how I pass the name of the pipeline to the command.

If the pipeline name is given, we invoke the pipeline.

I then glued everything together using the following configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="admin:Restart" type="Sitecore.Sandbox.Commands.Admin.Restart, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <restartClient>
        <processor type="Sitecore.Sandbox.Pipelines.RestartRestarter.RestartRestarterOperations, Sitecore.Sandbox">
          <Restarter type="Sitecore.Sandbox.Utilities.Restarters.SitecoreClientRestarter, Sitecore.Sandbox"/>
        </processor>  
      </restartClient>
      <restartServer>
        <processor type="Sitecore.Sandbox.Pipelines.RestartRestarter.RestartRestarterOperations, Sitecore.Sandbox">
          <Restarter type="Sitecore.Sandbox.Utilities.Restarters.SitecoreServerRestarter, Sitecore.Sandbox"/>
        </processor>
      </restartServer>
    </pipelines>
  </sitecore>
</configuration>

I’m omitting how I created custom buttons in a custom ribbon in Sitecore to test this. If you want to learn about adding buttons to the Sitecore Ribbon, please read John West’s blog post on doing so.

However, I did do the following to pass the name of the pipeline we want to invoke in the custom command class defined above:

restart-client-button

I wish I could show you some screenshots on how this works. However, there really isn’t much visual to see here.

If you have any suggestions on how I could show this in action, or improve the code above, please share in a comment.


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

$
0
0

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

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

close-all-but-this-vs

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

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

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

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

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

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

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

using System.Linq;

using Sitecore.Diagnostics;

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

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

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

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

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

using System.Collections.Generic;

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

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

        public bool ShouldDelete { get; set; }

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

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

using System;
using System.Collections.Generic;

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

using Sitecore.Sandbox.Pipelines.GetSiblings;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            return CommandState.Enabled;
        }

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

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

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

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

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

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

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

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

Let’s see this in action.

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

stuff-to-delete

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

delete-all-but-this-context-menu

I was then prompted with a confirmation dialog:

delete-all-but-this-confirmation-dialog

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

items-deleted

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

Until next time, have a Sitecoretastic day!


Remove Buttons from Field Types in Sitecore

$
0
0

This topic has been in my queue for quite some time, and I felt it was long overdue for me to write something up about it.

The idea for this post originated from a comment by Sitecore MVP Dan Solovay on my post covering how to resolve media library item aliases.

Dan had asked about the business need for resolving media library item aliases. This question got me thinking: perhaps one might not want to resolve media library aliases.

So what can be done to prevent media library items from being linked in Sitecore alias items? Well, you could remove the ‘Insert Media Link’ button from the General Link field type.

Removing buttons from existing field types in Sitecore is extremely easy, and no code changes are required. All you have to do is delete “Menu” items under field types in the Core database — although I would recommend that you duplicate an existing field type which results in a new field type, and then delete “Menu” items under the duplicate item just as I’ve done here on the General Link field type:

remove-buttons-core-db

You might be asking why I deleted the WebEdit Buttons folder which drives the insert link functionality for the Page Editor. Removing buttons from the Page Editor isn’t as straightforward as deleting items in Sitecore, and I might cover this in a future blog post.

I then used my new field type:

chose-field-type

As you can see buttons were removed:

field-on-item-less-buttons

If you have any examples of where you had to remove buttons from field types in Sitecore, please drop a comment.

In a future post, I will cover adding new buttons to field types — unlike this post, such a change requires adding code.

Until next time, have a Sitecoretastic day, and Happy New Year!


Add Buttons to Field Types in Sitecore

$
0
0

In a previous post I showed how one could go about removing a button from a field type in Sitecore, and felt a complementary post on adding a new button to a field type was in order.

In this post I will show you how I added a ‘Tomorrow’ button to a new Date field type — I duplicated the existing Date field type (you’ll see this later on in this post) — although I am uncertain how useful such a button would be. In other words, I’m not advocating the Date field type have a ‘Tomorrow’ button. I have created it only as an example.

I first subclassed Sitecore.Shell.Applications.ContentEditor.Date in Sitecore.Kernel.dll so that our new Date control — I’ve named this DateExtended — contains all functionality of the “out of the box” Date control:

using System;

using Sitecore.Diagnostics;
using Sitecore.Web.UI.Sheer;
using Sitecore.Shell.Applications.ContentEditor;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class DateExtended : Date
    {
        public override void HandleMessage(Message message)
        {
            if (!ShouldHandleMessage(message))
            {
                return;
            }

            if (IsTomorrowClick(message))
            {
                SetDateTomorrow();
                return;
            }

            base.HandleMessage(message);
        }

        private bool ShouldHandleMessage(Message message)
        {
            return IsCurrentControl(message)
                    && !string.IsNullOrWhiteSpace(message.Name);
        }

        private bool IsCurrentControl(Message message)
        {
            return AreEqualIgnoreCase(message["id"], ID);
        }

        private static bool IsTomorrowClick(Message message)
        {
            return AreEqualIgnoreCase(message.Name, "contentdate:tomorrow");
        }

        private void SetDateTomorrow()
        {
            SetValue(GetDateTomorrow());
        }

        protected virtual string GetDateTomorrow()
        {
            return DateUtil.ToIsoDate(System.DateTime.Today.AddDays(1));
        }

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

I overrode the HandleMessage() method above to ascertain whether we should handle the message passed to it — only the “contentdate:tomorrow” message should be handled, and this is passed when the ‘Tomorrow’ button is clicked (later on in this post, you will see how this message is associated with the ‘Tomorrow’ button in Sitecore) — and delegate to the base class when another message is passed, and that message is for the current control (this is when message["id"] is equal to the ID property on the control).

If the ‘Tomorrow’ button was clicked, we derive tomorrow’s date, convert it to the ISO date format, and set it as the value of the field via the SetValue() method which is defined in the Sitecore.Shell.Applications.ContentEditor.Date class.

I then registered our library of controls — a library composed of the one lonely control shown above — with Sitecore via a patch configuration file:

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

I then duplicated the Date field type to a new field type:

new-field-type-core-db

The Control field contains the “content” prefix defined in the patch configuration file above and the name of the DateExtended class — both are joined together by a colon.

Under the new field type I added the new Menu item (button) for ‘Tomorrow’, and associated the “contentdate:tomorrow” message with it:

tomorrow-button-core-db

Let’s try this out!

I created a field on a template using our new field type above:

new-field-date-extended-type

I navigated to an item using the template above, and clicked the ‘Tomorrow’ button:

clicked-tomorrow-button

As you can see the next day’s date was set on the field.

If you have any examples of where you had to add a new button to an existing field type, or ideas for new buttons that might be useful, please share in a comment.


Periodically Rebuild Link Databases using an Agent in Sitecore

$
0
0

Last week a colleague had asked me whether rebuilding the Link Database would solve an issue she was seeing. That conversation got me thinking: wouldn’t it be nice if we could automate the rebuilding of the Link Database for each Sitecore database at a scheduled time?

I am certain others have already created solutions to do this — if you know of any, please share in a comment — but I didn’t conduct a search to find any (I normally advocate not reinventing the wheel for code solutions but wanted to have some fun building a new solution).

In the spirit of my post on putting Sitecore to work for you, I built the following Sitecore agent (check out John West’s blog post on Sitecore agents to learn more):

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Jobs;

namespace Sitecore.Sandbox.Tasks
{
    public class RebuildLinkDatabasesAgent
    {
        private static readonly IList<Database> Databases = new List<Database>();
        private static readonly Stopwatch Stopwatch = Stopwatch.StartNew();

        public void Run()
        {
            JobManager.Start(CreateNewJobOptions());
        }

        protected virtual JobOptions CreateNewJobOptions()
        {
            return new JobOptions("RebuildLinkDatabasesAgent", "index", Context.Site.Name, this, "RebuildLinkDatabases");
        }

        protected virtual void RebuildLinkDatabases()
        {
            Job job = Context.Job;
            try
            {
                RebuildLinkDatabases(Databases);
            }
            catch (Exception ex)
            {
                job.Status.Failed = true;
                job.Status.Messages.Add(ex.ToString());
            }

            job.Status.State = JobState.Finished;
        }

        private void RebuildLinkDatabases(IEnumerable<Database> databases)
        {
            Assert.ArgumentNotNull(databases, "databases");
            foreach (Database database in databases)
            {
                Stopwatch.Start();
                RebuildLinkDatabase(database);
                Stopwatch.Stop();
                LogEntry(database, Stopwatch.Elapsed.Milliseconds);
            }
        }

        protected virtual void RebuildLinkDatabase(Database database)
        {
            Assert.ArgumentNotNull(database, "database");
            Globals.LinkDatabase.Rebuild(database);
        }

        protected virtual void LogEntry(Database database, int elapsedMilliseconds)
        {
            Assert.ArgumentNotNull(database, "database");
            if (string.IsNullOrWhiteSpace(LogEntryFormat))
            {
                return;
            }

            Log.Info(string.Format(LogEntryFormat, database.Name, elapsedMilliseconds), this);
        }

        private static void AddDatabase(XmlNode configNode)
        {
            if (configNode == null || string.IsNullOrWhiteSpace(configNode.InnerText))
            {
                return;
            }

            Database database = TryGetDatabase(configNode.InnerText);
            if (database != null)
            {
                Databases.Add(database);
            }
        }

        private static Database TryGetDatabase(string databaseName)
        {
            Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            try
            {
                return Factory.GetDatabase(databaseName);
            }
            catch (Exception ex)
            {
                Type agentType = typeof(RebuildLinkDatabasesAgent);
                Log.Error(agentType.ToString(), ex, agentType);
            }

            return null;
        }

        private string LogEntryFormat { get; set; }
    }
}

Logic in the class above reads in a list of databases set in a configuration file, adds them to a list for processing — these are only added to the list if they exist — and rebuilds the Link Database in each via a Sitecore job.

I added some timing logic to see how long it takes to rebuild each database, and capture this information in the Sitecore log.

I then wired up the above class in Sitecore using the following patch include configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <scheduling>
      <agent type="Sitecore.Sandbox.Tasks.RebuildLinkDatabasesAgent" method="Run" interval="00:01:00">
        <databases hint="raw:AddDatabase">
          <database>core</database>
          <database>master</database>
          <database>web</database>
        </databases>
        <LogEntryFormat>Rebuilt link database: {0} in {1} milliseconds.</LogEntryFormat>
      </agent>
    </scheduling>
  </sitecore>
</configuration>

I’ve set this agent to run every minute for testing, but it would probably be wise to have this run no more than once or twice a day.

After waiting a bit, I saw the following in my Sitecore log:

rebuilt-link-database

I do question the rebuild times. These seem quite small, especially when it takes a while to rebuild the Link Databases via the Sitecore Control Panel. If you have any ideas/thoughts on why there is an incongruence between the times in my log and how long it takes to rebuild these via the Sitecore Control Panel, please share in a comment.

Further, if you have any recommendations on making this code better, or have other ideas on automating the rebuilding of Link Databases in Sitecore, please drop a comment.

Until next time, have a Sitecoretastic day!


Synchronize IDTable Entries Across Multiple Sitecore Databases Using a Composite IDTableProvider

$
0
0

The other day Sitecore MVP Kyle Heon asked:

This tweet got the wheels turning — more like got the hamster wheel spinning in my head — and I began experimenting on ways to synchronize IDTable entries across different Sitecore databases.

In this post, I will show the first solution I had come up with — yes I’ve come up with two solutions to this although no doubt there are more (if you have ideas for other solutions, or have tackled this problem in the past, please share in a comment) — but before I show that solution, I’d like to explain what the IDTable in Sitecore is, and why you might want to use it.

Assuming you’re using SQL Server for Sitecore, the “out of the box” IDTable in Sitecore is a database table that lives in all Sitecore databases — though Sitecore’s prepackaged configuration only points to the IDTable in the master database.

One has the ability to store key/value pairs in this table in the event you don’t want to “roll your own” custom database table — or use some other data store — and don’t want to expose these key/value pair relationships in Items in Sitecore.

The key must be a character string, and the value must be a Sitecore Item ID.

Plus, one has the ability to couple these key/value pairs with “custom data” — this, like the key, is stored in the database as a string which makes it cumbersome around storing complex data structures (having some sort of serialization/deserialization paradigm in place would be required to make this work).

Alex Shyba showed how one could leverage the IDTable for URLs for fictitious products stored in Sitecore in this post, and this article employed the IDTable for a custom Data Provider.

If you would like to know more about the IDTable, please leave a comment, and I will devote a future post to it.

Let’s take a look at the first solution I came up with:

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

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.IDTables;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Data.IDTables;

namespace Sitecore.Sandbox.Data.SqlServer
{
    public class CompositeSqlServerIDTable : IDTableProvider
    {
        private IDictionary<string, IDTableProvider> _IDTableProviders;
        private IDictionary<string, IDTableProvider> IDTableProviders
        {
            get
            {
                if (_IDTableProviders == null)
                {
                    _IDTableProviders = new Dictionary<string, IDTableProvider>();
                }

                return _IDTableProviders;
            }
        }

        private string DatabaseName { get; set; }

        public CompositeSqlServerIDTable()
            : this(GetDefaultDatabaseName())
        {
        }

        public CompositeSqlServerIDTable(string databaseName)
        {
            SetDatabaseName(databaseName);
        }

        private void SetDatabaseName(string databaseName)
        {
            Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            DatabaseName = databaseName;
        }

        public override void Add(IDTableEntry entry)
        {
            foreach (IDTableProvider provider in IDTableProviders.Values)
            {
                provider.Add(entry);
            }
        }

        public override IDTableEntry GetID(string prefix, string key)
        {
            return GetContextIDTableProvider().GetID(prefix, key);
        }

        public override IDTableEntry[] GetKeys(string prefix)
        {
            return GetContextIDTableProvider().GetKeys(prefix);
        }

        public override IDTableEntry[] GetKeys(string prefix, ID id)
        {
            return GetContextIDTableProvider().GetKeys(prefix, id);
        }

        protected virtual IDTableProvider GetContextIDTableProvider()
        {
            IDTableProvider provider;
            if (IDTableProviders.TryGetValue(DatabaseName, out provider))
            {
                return provider;
            }

            return new NullIDTable();
        }

        public override void Remove(string prefix, string key)
        {
            foreach (IDTableProvider provider in IDTableProviders.Values)
            {
                provider.Remove(prefix, key);
            }
        }

        protected virtual void AddIDTable(XmlNode configNode)
        {
            if (configNode == null || string.IsNullOrWhiteSpace(configNode.InnerText))
            {
                return;
            }

            IDTableProvider idTable = Factory.CreateObject<IDTableProvider>(configNode);
            if (idTable == null)
            {
                Log.Error("Configuration invalid for an IDTable!", this);
                return;
            }

            XmlAttribute idAttribute = configNode.Attributes["id"];
            if (idAttribute == null || string.IsNullOrWhiteSpace(idAttribute.Value))
            {
                Log.Error("IDTable configuration should have an id attribute set!", this);
                return;
            }

            if(IDTableProviders.ContainsKey(idAttribute.Value))
            {
                Log.Error("Duplicate IDTable id encountered!", this);
                return;
            }

            IDTableProviders.Add(idAttribute.Value, idTable);
        }

        private static string GetDefaultDatabaseName()
        {
            if (Context.ContentDatabase != null)
            {
                return Context.ContentDatabase.Name;
            }

            return Context.Database.Name;
        }
    }
}

The above class uses the Composite design pattern — it adds/removes entries in multiple IDTableProvider instances (these instances are specified in the configuration file that is shown later in this post), and delegates calls to the GetKeys and GetID methods of the instance that is referenced by the database name passed in to the class’ constructor.

The CompositeSqlServerIDTable class also utilizes a Null Object by creating an instance of the following class when the context database does not exist in the collection:

using System.Collections.Generic;

using Sitecore.Data;
using Sitecore.Data.IDTables;

namespace Sitecore.Sandbox.Data.IDTables
{
    public class NullIDTable : IDTableProvider
    {
        public NullIDTable()
        {
        }

        public override void Add(IDTableEntry entry)
        {
            return;
        }

        public override IDTableEntry GetID(string prefix, string key)
        {
            return null;
        }

        public override IDTableEntry[] GetKeys(string prefix)
        {
            return new List<IDTableEntry>().ToArray();
        }

        public override IDTableEntry[] GetKeys(string prefix, ID id)
        {
            return new List<IDTableEntry>().ToArray();
        }

        public override void Remove(string prefix, string key)
        {
            return;
        }
    }
}

The NullIDTable class above basically has no behavior, returns null for the GetID method, and an empty collection for both GetKeys methods. Having a Null Object helps us avoid checking for nulls when performing operations on the IDTableProvider instance when an instance cannot be found in the IDTableProviders Dictionary property — although we lose visibility around the context database not being present in the Dictionary (I probably should’ve included some logging code to capture this).

I then glued everything together using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <IDTable patch:instead="IDTable[@type='Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel']" type="Sitecore.Sandbox.Data.$(database).Composite$(database)IDTable" singleInstance="true">
      <IDTables hint="raw:AddIDTable">
        <IDTable id="core" type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true">
          <param connectionStringName="$(id)"/>
          <param desc="cacheSize">500KB</param>
        </IDTable>
        <IDTable id="master" type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true">
          <param connectionStringName="$(id)"/>
          <param desc="cacheSize">500KB</param>
        </IDTable>
        <IDTable id="web" type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true">
          <param connectionStringName="$(id)"/>
          <param desc="cacheSize">500KB</param>
        </IDTable>
      </IDTables>
    </IDTable>
  </sitecore>
</configuration>

To test the code above, I created the following web form which basically invokes add/remove methods on the instance of our IDTableProvider, and displays the entries in our IDTables after each add/remove operation:

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

using Sitecore.Configuration;
using Sitecore.Data.IDTables;

namespace Sandbox
{
    public partial class IDTableTest : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            AddEntries();
            ShowEntries();
            RemoveOneEntry();
            ShowEntries();
            RemoveAllEntries();
            ShowEntries();
        }

        private void AddEntries()
        {
            Response.Write("Adding two entries...<br /><br />");
            IDTable.Add("IDTableTest", "/mycoolhomepage1.aspx", Sitecore.Data.ID.Parse("{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}"));
            IDTable.Add("IDTableTest", "/someotherpage1.aspx", Sitecore.Data.ID.Parse("{BAB78AE5-8118-4476-B1B5-F8981DAE1779}"));
        }

        private void RemoveOneEntry()
        {
            Response.Write("Removing one entry...<br /><br />");
            IDTable.RemoveKey("IDTableTest", "/mycoolhomepage1.aspx");
        }

        private void RemoveAllEntries()
        {
            Response.Write("Removing all entries...<br /><br />");
            foreach (IDTableEntry entry in IDTable.GetKeys("IDTableTest"))
            {
                IDTable.RemoveKey(entry.Prefix, entry.Key);
            }
        }

        private void ShowEntries()
        {
            ShowEntries("core");
            ShowEntries("master");
            ShowEntries("web");
        }

        private void ShowEntries(string databaseName)
        {
            IDTableProvider provider = CreateNewIDTableProvider(databaseName);
            IEnumerable<IDTableEntry> entries = IDTable.GetKeys("IDTableTest");
            Response.Write(string.Format("{0} entries in IDTable in the {1} database:<br />", entries.Count(), databaseName));
            foreach (IDTableEntry entry in provider.GetKeys("IDTableTest"))
            {
                Response.Write(string.Format("key: {0} id: {1}<br />", entry.Key, entry.ID));
            }
            Response.Write("<br />");
        }

        protected virtual IDTableProvider CreateNewIDTableProvider(string databaseName)
        {
            return Factory.CreateObject(string.Format("//IDTable[@id='{0}']", databaseName), true) as IDTableProvider;
        }
    }
}

When I loaded up the web form above in a browser, I saw that things were working as expected:

idtable-composite-test-run

Although I had fun in writing all of the code above, I feel this isn’t exactly ideal for synchronizing entries in IDTables across multiple Sitecore databases. I cannot see why one would want an entry for a Item in master that has not yet been published to the web database.

If you know of a scenario where the above code could be useful, or have suggestions on making it better, please drop a comment.

In my next post, I will show you what I feel is a better approach to synchronizing entries across IDTables — a solution that synchronizes entries via publishing and Item deletion.

Until next time, have a Sitecorelicious day!


Synchronize IDTable Entries Across Multiple Sitecore Databases Using a Custom publishItem Pipeline Processor

$
0
0

In a previous post I showed a solution that uses the Composite design pattern in an attempt to answer the following question by Sitecore MVP Kyle Heon:

Although I enjoyed building that solution, it isn’t ideal for synchronizing IDTable entries across multiple Sitecore databases — entries are added to all configured IDTables even when Items might not exist in all databases of those IDTables (e.g. the Sitecore Items have not been published to those databases).

I came up with another solution to avoid the aforementioned problem — one that synchronizes IDTable entries using a custom <publishItem> pipeline processor, and the following class contains code for that processor:

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

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.IDTables;
using Sitecore.Diagnostics;
using Sitecore.Publishing.Pipelines.PublishItem;

namespace Sitecore.Sandbox.Pipelines.Publishing
{
    public class SynchronizeIDTables : PublishItemProcessor
    {
        private IEnumerable<string> _IDTablePrefixes;
        private IEnumerable<string> IDTablePrefixes
        {
            get
            {
                if (_IDTablePrefixes == null)
                {
                    _IDTablePrefixes = GetIDTablePrefixes();
                }

                return _IDTablePrefixes;
            }
        }

        private string IDTablePrefixesConfigPath { get; set; }

        public override void Process(PublishItemContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.PublishOptions, "context.PublishOptions");
            Assert.ArgumentNotNull(context.PublishOptions.SourceDatabase, "context.PublishOptions.SourceDatabase");
            Assert.ArgumentNotNull(context.PublishOptions.TargetDatabase, "context.PublishOptions.TargetDatabase");
            IDTableProvider sourceProvider = CreateNewIDTableProvider(context.PublishOptions.SourceDatabase);
            IDTableProvider targetProvider = CreateNewIDTableProvider(context.PublishOptions.TargetDatabase);
            RemoveEntries(targetProvider, GetAllEntries(targetProvider, context.ItemId));
            AddEntries(targetProvider, GetAllEntries(sourceProvider, context.ItemId));
        }

        protected virtual IDTableProvider CreateNewIDTableProvider(Database database)
        {
            Assert.ArgumentNotNull(database, "database");
            return Factory.CreateObject(string.Format("IDTable[@id='{0}']", database.Name), true) as IDTableProvider;
        }

        protected virtual IEnumerable<IDTableEntry> GetAllEntries(IDTableProvider provider, ID itemId)
        {
            Assert.ArgumentNotNull(provider, "provider");
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(itemId), "itemId", "itemId cannot be null or empty!");
            List<IDTableEntry> entries = new List<IDTableEntry>();
            foreach(string prefix in IDTablePrefixes)
            {
                IEnumerable<IDTableEntry> entriesForPrefix = provider.GetKeys(prefix, itemId);
                if (entriesForPrefix.Any())
                {
                    entries.AddRange(entriesForPrefix);
                }
            }

            return entries;
        }

        private static void RemoveEntries(IDTableProvider provider, IEnumerable<IDTableEntry> entries)
        {
            Assert.ArgumentNotNull(provider, "provider");
            Assert.ArgumentNotNull(entries, "entries");
            foreach (IDTableEntry entry in entries)
            {
                provider.Remove(entry.Prefix, entry.Key);
            }
        }

        private static void AddEntries(IDTableProvider provider, IEnumerable<IDTableEntry> entries)
        {
            Assert.ArgumentNotNull(provider, "provider");
            Assert.ArgumentNotNull(entries, "entries");
            foreach (IDTableEntry entry in entries)
            {
                provider.Add(entry);
            }
        }

        protected virtual IEnumerable<string> GetIDTablePrefixes()
        {
            Assert.ArgumentNotNullOrEmpty(IDTablePrefixesConfigPath, "IDTablePrefixConfigPath");
            return Factory.GetStringSet(IDTablePrefixesConfigPath);
        }
    }
}

The Process method above grabs all IDTable entries for all defined IDTable prefixes — these are pulled from the configuration file that is shown later on in this post — from the source database for the Item being published, and pushes them all to the target database after deleting all preexisting entries from the target database for the Item (the code is doing a complete overwrite for the Item’s IDTable entries in the target database).

I also added the following code to serve as an item:deleted event handler (if you would like to learn more about events and their handlers, check out John West‘s post about them, and also take a look at this page on the
Sitecore Developer Network (SDN)) to remove entries for the Item when it’s being deleted:

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

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Events;
using Sitecore.Data.IDTables;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Events;

namespace Sitecore.Sandbox.Data.IDTables
{
    public class ItemEventHandler
    {
        private IEnumerable<string> _IDTablePrefixes;
        private IEnumerable<string> IDTablePrefixes
        {
            get
            {
                if (_IDTablePrefixes == null)
                {
                    _IDTablePrefixes = GetIDTablePrefixes();
                }

                return _IDTablePrefixes;
            }
        }

        private string IDTablePrefixesConfigPath { get; set; }

        protected void OnItemDeleted(object sender, EventArgs args)
        {
            if (args == null)
            {
                return;
            }

            Item item = Event.ExtractParameter(args, 0) as Item;
            if (item == null)
            {
                return;
            }

            DeleteItemEntries(item);
        }

        private void DeleteItemEntries(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            IDTableProvider provider = CreateNewIDTableProvider(item.Database.Name);
            foreach (IDTableEntry entry in GetAllEntries(provider, item.ID))
            {
                provider.Remove(entry.Prefix, entry.Key);
            }
        }

        protected virtual IEnumerable<IDTableEntry> GetAllEntries(IDTableProvider provider, ID itemId)
        {
            Assert.ArgumentNotNull(provider, "provider");
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(itemId), "itemId", "itemId cannot be null or empty!");
            List<IDTableEntry> entries = new List<IDTableEntry>();
            foreach (string prefix in IDTablePrefixes)
            {
                IEnumerable<IDTableEntry> entriesForPrefix = provider.GetKeys(prefix, itemId);
                if (entriesForPrefix.Any())
                {
                    entries.AddRange(entriesForPrefix);
                }
            }

            return entries;
        }

        private static void RemoveEntries(IDTableProvider provider, IEnumerable<IDTableEntry> entries)
        {
            Assert.ArgumentNotNull(provider, "provider");
            Assert.ArgumentNotNull(entries, "entries");
            foreach (IDTableEntry entry in entries)
            {
                provider.Remove(entry.Prefix, entry.Key);
            }
        }

        protected virtual IDTableProvider CreateNewIDTableProvider(string databaseName)
        {
            return Factory.CreateObject(string.Format("IDTable[@id='{0}']", databaseName), true) as IDTableProvider;
        }

        protected virtual IEnumerable<string> GetIDTablePrefixes()
        {
            Assert.ArgumentNotNullOrEmpty(IDTablePrefixesConfigPath, "IDTablePrefixConfigPath");
            return Factory.GetStringSet(IDTablePrefixesConfigPath);
        }
    }
}

The above code retrieves all IDTable entries for the Item being deleted — filtered by the configuration defined IDTable prefixes — from its database’s IDTable, and calls the Remove method on the IDTableProvider instance that is created for the Item’s database for each entry.

I then registered all of the above in Sitecore using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <events>
      <event name="item:deleted">
        <handler type="Sitecore.Sandbox.Data.IDTables.ItemEventHandler, Sitecore.Sandbox" method="OnItemDeleted">
          <IDTablePrefixesConfigPath>IDTablePrefixes/IDTablePrefix</IDTablePrefixesConfigPath>
        </handler>
      </event>
    </events>
    <IDTable type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true">
      <patch:attribute name="id">master</patch:attribute>
      <param connectionStringName="master"/>
      <param desc="cacheSize">500KB</param>
    </IDTable>
    <IDTable id="web" type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true">
      <param connectionStringName="web"/>
      <param desc="cacheSize">500KB</param>
    </IDTable>
    <IDTablePrefixes>
      <IDTablePrefix>IDTableTest</IDTablePrefix>
    </IDTablePrefixes>
    <pipelines>
      <publishItem>
        <processor type="Sitecore.Sandbox.Pipelines.Publishing.SynchronizeIDTables, Sitecore.Sandbox">
          <IDTablePrefixesConfigPath>IDTablePrefixes/IDTablePrefix</IDTablePrefixesConfigPath>
        </processor>
      </publishItem>
    </pipelines>
  </sitecore>
</configuration>

For testing, I quickly whipped up a web form to add a couple of IDTable entries using an IDTableProvider for the master database — I am omitting that code for brevity — and ran a query to verify the entries were added into the IDTable in my master database (I also ran another query for the IDTable in my web database to show that it contains no entries):

idtables-before-publish

I published both items, and queried the IDTable in the master and web databases:

idtables-after-publish-both-items

As you can see, both entries were inserted into the web database’s IDTable.

I then deleted one of the items from the master database via the Sitecore Content Editor:

idtables-deleted-from-master

It was removed from the IDTable in the master database.

I then published the deleted item’s parent with subitems:

idtables-published-deletion

As you can see, it was removed from the IDTable in the web database.

If you have any suggestions for making this code better, or have another solution for synchronizing IDTable entries across multiple Sitecore databases, please share in a comment.



Rename Sitecore Clones When Renaming Their Source Item

$
0
0

Earlier today I discovered that clones in Sitecore are not renamed when their source Items are renamed — I’m baffled over how I have not noticed this before since I’ve been using Sitecore clones for a while now :-/

I’ve created some clones in my Sitecore instance to illustrate:

clones-not-renamed-yet

I then initiated the process for renaming the source item:

clones-renaming

As you can see the clones were not renamed:

clones-not-renamed-sad-face

One might argue this is expected behavior for clones — only source Item field values are propagated to its clones when there are no data collisions (i.e. a source Item’s field value is pushed to the same field in its clone when that data has not changed directly on the clone — and the Item name should not be included in this process since it does not live in a field.

Sure, I see that point of view but one of the requirements of the project I am currently working on mandates that source Item name changes be pushed to the clones of that source Item.

So what did I do to solve this? I created an item:renamed event handler similar to the following (the one I built for my project is slightly different though the idea is the same):

using System;
using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Events;
using Sitecore.Links;
using Sitecore.SecurityModel;

namespace Sitecore.Sandbox.Data.Clones
{
    public class ItemEventHandler
    {
        protected void OnItemRenamed(object sender, EventArgs args)
        {
            Item item = GetItem(args);
            if (item == null)
            {
                return;
            }

            RenameClones(item);
        } 

        protected virtual Item GetItem(EventArgs args)
        {
            if (args == null)
            {
                return null;
            }

            return Event.ExtractParameter(args, 0) as Item;
        }

        protected virtual void RenameClones(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            using (new LinkDisabler())
            {
                using (new SecurityDisabler())
                {
                    using (new StatisticDisabler())
                    {
                        Rename(GetClones(item), item.Name);
                    }
                }
            }
        }

        protected virtual IEnumerable<Item> GetClones(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            IEnumerable<Item> clones = item.GetClones();
            if (clones == null)
            {
                return new List<Item>();
            }

            return clones;
        }

        protected virtual void Rename(IEnumerable<Item> items, string newName)
        {
            Assert.ArgumentNotNull(items, "items");
            Assert.ArgumentNotNullOrEmpty(newName, "newName");
            foreach (Item item in items)
            {
                Rename(item, newName);
            }
        }

        protected virtual void Rename(Item item, string newName)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNullOrEmpty(newName, "newName");
            if (!item.Access.CanRename())
            {
                return;
            }
            
            item.Editing.BeginEdit();
            item.Name = newName;
            item.Editing.EndEdit();
        }
    }
}

The handler above retrieves all clones for the Item being renamed, and renames them using the new name of the source Item — I borrowed some logic from the Execute method in Sitecore.Shell.Framework.Pipelines.RenameItem in Sitecore.Kernel.dll (this serves as a processor of the <uiRenameItem> pipeline).

If you would like to learn more about events and their handlers, I encourage you to check out John West‘s post about them, and also take a look at this page on the
Sitecore Developer Network (SDN).

I then registered the above event handler in Sitecore using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <events>
      <event name="item:renamed">
        <handler type="Sitecore.Sandbox.Data.Clones.ItemEventHandler, Sitecore.Sandbox" method="OnItemRenamed"/>
      </event>
    </events>
  </sitecore>
</configuration>

Let’s take this for a spin.

I went back to my source item, renamed it back to ‘My Cool Item’, and then initiated another rename operation on it:

clones-renaming

As you can see all clones were renamed:

clones-renamed

If you have any thoughts/concerns on this approach, or ideas on other ways to accomplish this, please share in a comment.


Prevent Sitecore Dictionary Entry Keys From Appearing When Their Phrase Field is Empty

$
0
0

Earlier today when doing research for another blog post around on-demand language translation in Sitecore, I remembered I wanted to blog about an issue I saw a while back when using the Sitecore Dictionary, but before I dive into that issue — and a possible approach for resolving it — let me give you a little information on what the Sitecore Dictionary is, and why you might want to use it — actually you probably should use it!

The Sitecore Dictionary is a place in Sitecore where you can store multilingual content for labels or string literals in your code (this could be front-end code, or even content displayed in the Sitecore shell). I’ve created the following Dictionary entry as an example:

coffee-dictionary-entry-set

The “coffee” item above is the Dictionary entry, and its parent item “beverage types” is a Dictionary folder.

You could use a sublayout like the following to display the text stored in the Phrase field on the front-end of your website:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Translate Test.ascx.cs" Inherits="Sandbox.layouts.sublayouts.Translate_Test" %>
<h2>Dictionary Test</h2>
Key => <asp:Literal ID="litKey" runat="server" /><br />
Phrase => <asp:Literal ID="litTranslateTest" runat="server" />

The code-behind of the sublayout:

using System;

using Sitecore.Globalization;

namespace Sandbox.layouts.sublayouts
{
    public partial class Translate_Test : System.Web.UI.UserControl
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            string key = "beveragetypes.coffee";
            litKey.Text = key;
            litTranslateTest.Text = Translate.Text(key);
        }
    }
}

In the Page_Load method above, I’ve invoked Sitecore.Globalization.Translate.Text() to grab the value out of the Phrase field of the “coffee” Dictionary entry using its key. The Sitecore.Globalization.Translate.Text() method uses Sitecore.Context.Language to ascertain which language version of the Dictionary entry to use.

When I navigated to the page that has the sublayout above mapped to its presentation, I see the “coffee” entry’s Phrase appear:

coffee-dictionary-entry-set-front-end

Let’s see how this works using another language version of this Dictionary entry. I added a Danish version for our “coffee” entry:

coffee-dictionary-entry-set-danish

I navigated to my page again after embedding the Danish language code in its URL to get the Danish version of this Dictionary entry:

coffee-dictionary-entry-set-front-end-danish

As you can see the Danish version appeared, and I did not have to write any additional code to make this happen.

Well, this is great and all until someone forgets to include a phrase for a Dictionary entry:

coffee-dictionary-entry-not-set

When we go to the front-end, we see that the Dictionary entry’s key appears instead of its phrase:

coffee-dictionary-entry-empty-front-end-problem

As a fix for this, I created the following class to serve as a processor for the <getTranslation> pipeline (this pipeline was introduced in Sitecore 6.6):

using System.Collections.Generic;

using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetTranslation;

namespace Sitecore.Sandbox.Pipelines.GetTranslation
{
    public class SetAsEmpty
    {
        private IList<string> _KeyPrefixes;
        private IList<string> KeyPrefixes 
        {
            get
            {
                if (_KeyPrefixes == null)
                {
                    _KeyPrefixes = new List<string>();
                }

                return _KeyPrefixes;
            }
        }

        public void Process(GetTranslationArgs args)
        {
            if (!ShouldSetAsEmpty(args))
            {
                return;
            }

            args.Result = string.Empty;
        }

        protected virtual bool ShouldSetAsEmpty(GetTranslationArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            return args.Result == null && HasKeyPrefix(args.Key);
        }

        protected virtual bool HasKeyPrefix(string key)
        {
            if (string.IsNullOrWhiteSpace(key))
            {
                return false;
            }

            foreach (string keyPrefix in KeyPrefixes)
            {
                if (key.StartsWith(keyPrefix))
                {
                    return true;
                }
            }

            return false;
        }

        protected virtual void AddKeyPrefix(string keyPrefix)
        {
            if(string.IsNullOrWhiteSpace(keyPrefix))
            {
                return;
            }

            KeyPrefixes.Add(keyPrefix);
        }
    }
}

The idea here is to check to see if the Dictionary entry’s key starts with a configuration defined prefix, and if it does, set the GetTranslationArgs instance’s Result property to the empty string when it’s null.

The reason why we check for a specific prefix is to ensure we don’t impact other parts of Sitecore that use methods that leverage the <getTranslation> pipeline (I learned this the hard way when virtually all labels in my instance’s Content Editor disappeared before adding the logic above to check whether a Dictionary entry’s key started with a config defined prefix).

I then wired this up in Sitecore using the following configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getTranslation>
        <processor type="Sitecore.Sandbox.Pipelines.GetTranslation.SetAsEmpty, Sitecore.Sandbox">
          <keyPrefixes hint="list:AddKeyPrefix">
            <prefix>beveragetypes.</prefix>
          </keyPrefixes>
        </processor>
      </getTranslation>
    </pipelines>
  </sitecore>
</configuration>

When I navigated back to my page, I see that nothing appears for the Dictionary entry’s phrase since it was set to the empty string by our pipeline processor above.

coffee-dictionary-entry-empty-front-end

One thing I should note: I have only tested this in Sitecore 6.6, and I’m not aware if this Dictionary entry issue exists in Sitecore 7. If this issue was fixed in Sitecore 7, please share in a comment.

Plus, if you have any comments on this, or other ideas for solving this problem, please leave a comment.


Add Sitecore Rocks Commands to Protect and Unprotect Items

$
0
0

The other day I read this post where the author showcased a new Clipboard command he had added into Sitecore Rocks, and immediately wanted to experiment with adding my own custom command into Sitecore Rocks.

After some research, I stumbled upon this post which gave a walk-through on augmenting Sitecore Rocks by adding a Server Component — this is an assembled library of code for your Sitecore instance to handle requests from Sitecore Rocks — and a Plugin — this is an assembled library of code that can contain custom commands — and decided to follow its lead.

I first created a Sitecore Rocks Server Component project in Visual Studio:

sitecore-rocks-server-component

After some pondering, I decided to ‘cut my teeth’ on creating custom commands to protect and unprotect Items in Sitecore (for more information on protecting/unprotecting Items in Sitecore, check out ‘How to Protect or Unprotect an Item’ in Sitecore’s Client Configuration
Cookbook
).

I decided to use the template method pattern for the classes that will handle requests from Sitecore Rocks — I envisioned some shared logic across the two — and put this shared logic into the following base class:

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

namespace Sitecore.Rocks.Server.Requests
{
    public abstract class EditItem
    {
        public string Execute(string id, string databaseName)
        {
            Assert.ArgumentNotNullOrEmpty(id, "id");
            return ExecuteEditItem(GetItem(id, databaseName));
        }

        private string ExecuteEditItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            item.Editing.BeginEdit();
            string response = UpdateItem(item);
            item.Editing.EndEdit();
            return response;
        }

        protected abstract string UpdateItem(Item item);
        
        private static Item GetItem(string id, string databaseName)
        {
            Assert.ArgumentNotNullOrEmpty(id, "id");
            Database database = GetDatabase(databaseName);
            Assert.IsNotNull(database, string.Format("database: {0} does not exist!", databaseName));
            return database.GetItem(id);
        }

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

The EditItem base class above gets the Item in the requested database, and puts the Item into edit mode. It then passes the Item to the UpdateItem method — subclasses must implement this method — and then turns off edit mode for the Item.

As a side note, all Server Component request handlers must have a method named Execute.

For protecting a Sitecore item, I built the following subclass of the EditItem class above:

using Sitecore.Data.Items;

namespace Sitecore.Rocks.Server.Requests.Attributes
{
    public class ProtectItem : EditItem
    {
        protected override string UpdateItem(Item item)
        {
            item.Appearance.ReadOnly = true;
            return string.Empty;
        }
    }
}

The ProtectItem class above just protects the Item passed to it.

I then built the following subclass of EditItem to unprotect an item passed to its UpdateItem method:

using Sitecore.Data.Items;

namespace Sitecore.Rocks.Server.Requests.Attributes
{
    public class UnprotectItem : EditItem
    {
        protected override string UpdateItem(Item item)
        {
            item.Appearance.ReadOnly = false;
            return string.Empty;
        }
    }
}

I built the above Server Component solution, and put its resulting assembly into the /bin folder of my Sitecore instance.

I then created a Plugin solution to handle the protect/unprotect commands in Sitecore Rocks:

sitecore-rocks-plugin

I created the following command to protect a Sitecore Item:

using System;
using System.Linq;

using Sitecore.VisualStudio.Annotations;
using Sitecore.VisualStudio.Commands;
using Sitecore.VisualStudio.Data;
using Sitecore.VisualStudio.Data.DataServices;

using SmartAssembly.SmartExceptionsCore;

namespace Sitecore.Rocks.Sandbox.Commands
{
    [Command]
    public class ProtectItemCommand : CommandBase
    {
        public ProtectItemCommand()
        {
            Text = "Protect Item";
            Group = "Edit";
            SortingValue = 4010;
        }

        public override bool CanExecute([CanBeNull] object parameter)
        {
            IItemSelectionContext context = null;
            bool canExecute = false;
            try
            {
                context = parameter as IItemSelectionContext;
                canExecute = context != null && context.Items.Count() == 1 && !IsProtected(context.Items.FirstOrDefault());
            }
            catch (Exception ex)
            {
                StackFrameHelper.CreateException3(ex, context, this, parameter);
                throw;
            }

            return canExecute;
        }

        private static bool IsProtected(IItem item)
        {
            ItemVersionUri itemVersionUri = new ItemVersionUri(item.ItemUri, LanguageManager.CurrentLanguage, Sitecore.VisualStudio.Data.Version.Latest);
            Item item2 = item.ItemUri.Site.DataService.GetItemFields(itemVersionUri);
            foreach (Field field in item2.Fields)
            {
                if (string.Equals("__Read Only", field.Name, StringComparison.CurrentCultureIgnoreCase) && field.Value == "1")
                {
                    return true;
                }
            }

            return false;
        }

        public override void Execute([CanBeNull] object parameter)
        {
            IItemSelectionContext context = null;
            try
            {
                context = parameter as IItemSelectionContext;
                IItem item = context.Items.FirstOrDefault();
                item.ItemUri.Site.DataService.ExecuteAsync
                (
                    "Attributes.ProtectItem",
                    CreateEmptyCallback(),
                    new object[] 
                    { 
	                    item.ItemUri.ItemId.ToString(), 
	                    item.ItemUri.DatabaseName.ToString()
                    }
                );
            }
            catch (Exception ex)
            {
                StackFrameHelper.CreateException3(ex, context, this, parameter);
                throw;
            }
        }

        private ExecuteCompleted CreateEmptyCallback()
        {
            return (response, executeResult) => { return; };
        }
    }
}

The ProtectItemCommand command above is only displayed when the selected Item is not protected — this is ascertained by logic in the CanExecute method — and fires off a request to the Sitecore.Rocks.Server.Requests.Attributes.ProtectItem request handler in the Server Component above to protect the selected Item.

I then built the following command to do the exact opposite of the command above: only appear when the selected Item is protected, and make a request to Sitecore.Rocks.Server.Requests.Attributes.UnprotectItem — shown above in the Server Component — to unprotect the selected Item:

using System;
using System.Linq;

using Sitecore.VisualStudio.Annotations;
using Sitecore.VisualStudio.Commands;
using Sitecore.VisualStudio.Data;
using Sitecore.VisualStudio.Data.DataServices;

using SmartAssembly.SmartExceptionsCore;

namespace Sitecore.Rocks.Sandbox.Commands
{
    [Command]
    public class UnprotectItemCommand : CommandBase
    {
        public UnprotectItemCommand()
        {
            Text = "Unprotect Item";
            Group = "Edit";
            SortingValue = 4020;
        }

        public override bool CanExecute([CanBeNull] object parameter)
        {
            IItemSelectionContext context = null;
            bool canExecute = false;
            try
            {
                context = parameter as IItemSelectionContext;
                canExecute = context != null && context.Items.Count() == 1 && IsProtected(context.Items.FirstOrDefault());
            }
            catch (Exception ex)
            {
                StackFrameHelper.CreateException3(ex, context, this, parameter);
                throw;
            }

            return canExecute;
        }

        private static bool IsProtected(IItem item)
        {
            ItemVersionUri itemVersionUri = new ItemVersionUri(item.ItemUri, LanguageManager.CurrentLanguage, Sitecore.VisualStudio.Data.Version.Latest);
            Item item2 = item.ItemUri.Site.DataService.GetItemFields(itemVersionUri);
            foreach (Field field in item2.Fields)
            {
                if (string.Equals("__Read Only", field.Name, StringComparison.CurrentCultureIgnoreCase) && field.Value == "1")
                {
                    return true;
                }
            }

            return false;
        }

        public override void Execute([CanBeNull] object parameter)
        {
            IItemSelectionContext context = null;
            try
            {
                context = parameter as IItemSelectionContext;
                IItem item = context.Items.FirstOrDefault();
                item.ItemUri.Site.DataService.ExecuteAsync
                (
                    "Attributes.UnprotectItem",
                    CreateEmptyCallback(),
                    new object[] 
                    { 
	                    item.ItemUri.ItemId.ToString(), 
	                    item.ItemUri.DatabaseName.ToString()
                    }
                );
            }
            catch (Exception ex)
            {
                StackFrameHelper.CreateException3(ex, context, this, parameter);
                throw;
            }
        }

        private ExecuteCompleted CreateEmptyCallback()
        {
            return (response, executeResult) => { return; };
        }
    }
}

I had to do a lot of discovery in Sitecore.Rocks.dll via .NET Reflector in order to build the above commands, and had a lot of fun while searching and learning.

Unfortunately, I could not get the commands above to show up in the Sitecore Explorer context menu in my instance of Sitecore Rocks even though my plugin did make its way out to my C:\Users\[my username]\AppData\Local\Sitecore\Sitecore.Rocks\Plugins\ folder.

I troubleshooted for some time but could not determine why these commands were not appearing — if you have any ideas, please leave a comment — and decided to register my commands using Extensions in Sitecore Rocks as a fallback plan:

sitecore-rocks-extensions-menu-option

After clicking ‘Extensions’ in the Sitecore dropdown menu in Visual Studio, I was presented with the following dialog, and added my classes via the ‘Add’ button on the right:

sitecore-rocks-extension-dialog

Let’s see this in action.

I first created a Sitecore Item for testing:

item-unprotected

I navigated to that Item in the Sitecore Explorer in Sitecore Rocks, and right-clicked on it:

item-unprotected-1

After clicking ‘Protect Item’, I verified the Item was protected in Sitecore:

item-protected

I then went back to our test Item in the Sitecore Explorer of Sitecore Rocks, and right-clicked again:

sitecore-rocks-unprotect-item

After clicking ‘Unprotect Item’, I took a look at the Item in Sitecore, and saw that it was no longer protected:

item-unprotected-again

If you have any thoughts on this, or ideas for other commands that you would like to see in Sitecore Rocks, please drop a comment.

Until next time, have a Sitecoretastic day, and don’t forget: Sitecore Rocks!


Embedded Tweets in Sitecore: A Proof of Concept

$
0
0

In a previous post, I showcased a “proof of concept” for shortcodes in Sitecore — this is a shorthand notation for embedding things like YouTube videos in your webpages without having to type up a bunch of HTML — and felt I should follow up with another “proof of concept” around incorporating Embedded Tweets in Sitecore.

You might be asking “what’s an Embedded Tweet?” An Embedded Tweet is basically the process of pasting a Tweet URL from Twitter into an editable content area of your website/blog/whatever (think Rich Text field in Sitecore), and let the code that builds the HTML for your site figure out how to display it.

For example, I had used an Embedded Tweet in a recent post:

tweet-url-wordpress

This is what is seen on the rendered page:

tweet-embedded

While doing some research via Google on how to do this in Sitecore, I found this page from Twitter that discusses how you could go about accomplishing this, and discovered how to get JSON containing information about a Tweet — including its HTML — using one of Twitter’s API URLs:

tweet-api-json

The JSON above drove me to build the following POCO class to represent data returned by that URL:

using System.Runtime.Serialization;

namespace Sitecore.Sandbox.Pipelines.RenderField.Tweets
{
    public class Tweet
    {
        [DataMember(Name = "cache_age")]
        public int CacheAgeMilliseconds { get; set; }

        [DataMember(Name = "url")]
        public string Url { get; set; }

        [DataMember(Name = "html")]
        public string Html { get; set; }
    }
}

I decided to omit some of the JSON properties returned by the Twitter URL from my class above — width and height are examples — since I felt I did not need to use them for this “proof of concept”.

I then leveraged the class above in the following class that will serve as a <renderField> pipeline processor to embed Tweets:

using System;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;

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

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Sitecore.Sandbox.Pipelines.RenderField.Tweets
{
    public class ExpandTweets
    {
        private string TwitterWidgetScriptTag {get ; set; }

        private string TwitterApiUrlFormat { get; set; }

        private string _TweetPattern;
        private string TweetPattern 
        {
            get
            {
                return _TweetPattern;
            }
            set
            {
                _TweetPattern = value;
                if (!string.IsNullOrWhiteSpace(_TweetPattern))
                {
                    _TweetPattern = HttpUtility.HtmlDecode(_TweetPattern);
                }
            }
        }

        private HtmlCache _HtmlCache;
        private HtmlCache HtmlCache
        {
            get
            {
                if (_HtmlCache == null)
                {
                    _HtmlCache = CacheManager.GetHtmlCache(Context.Site);
                }

                return _HtmlCache;
            }
        }

        public void Process(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            AssertRequired();
            if(!ShouldFieldBeProcessed(args))
            {
                return;
            }

            args.Result.FirstPart = ExpandTweetUrls(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 void AssertRequired()
        {
            Assert.IsNotNullOrEmpty(TwitterWidgetScriptTag, "TwitterWidgetScriptTag must be set! Check your configuration!");
            Assert.IsNotNullOrEmpty(TwitterApiUrlFormat, "TwitterApiUrlFormat must be set! Check your configuration!");
            Assert.IsNotNullOrEmpty(TweetPattern, "TweetPattern must be set! Check your configuration!");
        }

        protected virtual string ExpandTweetUrls(string html)
        {
            string htmlExpanded = html;
            MatchCollection matches = Regex.Matches(htmlExpanded, TweetPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
            foreach (Match match in matches)
            {
                string tweetHtml = GetTweetHtml(match.Groups["id"].Value);
                if (!string.IsNullOrWhiteSpace(tweetHtml))
                {
                    htmlExpanded = htmlExpanded.Replace(match.Value, tweetHtml);
                }
            }

            if (matches.Count > 0)
            {
                htmlExpanded = string.Concat(htmlExpanded, TwitterWidgetScriptTag);
            }

            return htmlExpanded;
        }

        protected virtual string GetTweetHtml(string id)
        {
            string html = GetTweetHtmlFromCache(id);
            if (!string.IsNullOrWhiteSpace(html))
            {
                return html;
            }

            Tweet tweet = GetTweetFromApi(id);
            AddTweetHtmlToCache(id, tweet);
            return tweet.Html;
        }

        private string GetTweetHtmlFromCache(string id)
        {
            return HtmlCache.GetHtml(id);
        }

        private void AddTweetHtmlToCache(string id, Tweet tweet)
        {
            if (string.IsNullOrWhiteSpace(tweet.Html))
            {
                return;
            }

            if (tweet.CacheAgeMilliseconds > 0)
            {
                HtmlCache.SetHtml(id, tweet.Html, DateTime.Now.AddMilliseconds(tweet.CacheAgeMilliseconds));
                return;
            }

            HtmlCache.SetHtml(id, tweet.Html);
        }

        protected virtual Tweet GetTweetFromApi(string id)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(string.Format(TwitterApiUrlFormat, id));
            try
            {
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                {
                    var result = reader.ReadToEnd();
                    JObject jObject = JObject.Parse(result);
                    return JsonConvert.DeserializeObject<Tweet>(jObject.ToString());
                }
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }

            return new Tweet { Html = string.Empty };
        }
    }
}

Methods in the class above find all Tweet URLs in the Rich Text, Single-Line Text, or Multi-Line Text field being processed — the code determines if it’s a Tweet URL based on a pattern that is supplied by a configuration setting (you will see this below in this post); extract Tweets’ Twitter identifiers (these are located at the end of the Tweet URLs); and attempt to find the Tweets’ HTML in Sitecore’s HTML cache.

If the HTML is found in cache for a Tweet, we return it. Otherwise, we make a request to Twitter’s API to get it, put it in cache one we have it (it is set to expire after a specified number of milliseconds from the time it was retrieved: Twitter returns the number of milliseconds in one full year by default), and then we return it.

If the returned HTML is not empty, we replace it in the field’s value for display.

If the HTML returned is empty — this could happen when an exception is encountered during the Twitter API call (of course we log the exception in the Sitecore log when this happens ;) ) — we don’t touch the Tweet URL in the field’s value.

Once all Tweet URLs have been processed, we append a script tag referencing Twitter’s widget.js file — this is supplied through a configuration setting, and it does the heavy lifting on making the Tweet HTML look Twitterific ;) — to the field’s rendered HTML.

I then tied everything together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderField>
        <processor type="Sitecore.Sandbox.Pipelines.RenderField.Tweets.ExpandTweets, Sitecore.Sandbox"
					patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetTextFieldValue, Sitecore.Kernel']">
          <TwitterWidgetScriptTag>&lt;script async src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</TwitterWidgetScriptTag>
          <TwitterApiUrlFormat>https://api.twitter.com/1/statuses/oembed.json?id={0}&amp;omit_script=true</TwitterApiUrlFormat>
          <TweetPattern>https://twitter.com/.+/status/(?&lt;id&gt;\d*)</TweetPattern>
        </processor>
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Let’s see this in action!

I created a test Item, and added some legitimate and bogus Tweet URLs into one of its Rich Text fields (please pardon the grammatical issues in the following screenshots :-/):

tweets-rich-text

This isn’t the most aesthetically pleasing HTML, but it will serve its purpose for testing:

tweets-rich-text-html

After saving and publishing, I navigated to my test Item’s page, and saw this:

tweets-front-end

If you have any suggestions on making this better, or have other ideas for embedding Tweets in Sitecore, please share in a comment.


Restrict Certain Files from Being Attached to Web Forms for Marketers Forms in Sitecore

$
0
0

Last week I was given a task to research how to prevent certain files from being attached to Web Forms for Marketers (WFFM) forms: basically files that have certain extensions, or files that exceed a specified size.

I have not seen this done before in WFFM, so I did what comes naturally to me: I Googled! :)

After a few unsuccessful searches on the internet — if you have some examples on how others have accomplished this in WFFM, please share in a comment — I decided to dig into the WFFM assemblies to see what would be needed to accomplish this, and felt using custom WFFM field validators would be the way to go.

I thought having a custom validator to check the attached file’s MIME type would be a better solution over one that checks the file’s extension — thanks to Sitecore MVP Yogesh Patel for giving me the idea from his post on restricting certain files from being uploading into Sitecore by checking their MIME types — since a malefactor could attach a restricted file with a different extension to bypass the extension validation step.

That thought lead me to build the following custom WFFM field validator:

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

using Sitecore.Form.Core.Validators;
using Sitecore.Form.Web.UI.Controls;

namespace Sitecore.Sandbox.Form.Core.Validators
{
    public class RestrictedFileTypes : FormCustomValidator
    {
        public string MimeTypesNotAllowed
        {
            get
            {
                if (string.IsNullOrWhiteSpace(base.classAttributes["mimeTypesNotAllowed"]))
                {
                    return string.Empty;
                }

                return base.classAttributes["mimeTypesNotAllowed"];
            }
            set
            {
                base.classAttributes["mimeTypesNotAllowed"] = value;
            }
        }

        public RestrictedFileTypes()
        {
        }

        protected override bool OnServerValidate(string value)
        {
            IEnumerable<string> mimeTypesNotAllowed = GetMimeTypesNotAllowed();
            FileUpload fileUpload = FindControl(ControlToValidate) as FileUpload;
            bool canProcess = mimeTypesNotAllowed.Any() && fileUpload != null && fileUpload.HasFile;
            if (!canProcess)
            {
                return true;
            }

            foreach(string mimeType in mimeTypesNotAllowed)
            {
                if (AreEqualIgnoreCase(mimeType, fileUpload.PostedFile.ContentType))
                {
                    return false;
                }
            }

            return true;
        }

        private IEnumerable<string> GetMimeTypesNotAllowed()
        {
            if (string.IsNullOrWhiteSpace(MimeTypesNotAllowed))
            {
                return new List<string>();
            }

            return MimeTypesNotAllowed.Split(new []{',', ';'}, StringSplitOptions.RemoveEmptyEntries);
        }

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

Restricted MIME types are passed to the custom validator through a parameter named MimeTypesNotAllowed, and these are injected into a property of the same name.

The MIME types can be separated by commas or semicolons, and the code above splits the string along these delimiters into a collection, checks to see if the uploaded file — we get the uploaded file via the FileUpload control on the form for the field we are validating — has a restricted MIME type by iterating over the collection of restricted MIME types, and comparing each entry to its MIME type. If there is a match, we return “false”: basically the field is invalid.

If no MIME types were set for the validator, or no file was uploaded, we return “true”: the field is valid. We do this for the case where the field is not required, or there is a required field validator set for it, and we don’t want to interfere with its validation check.

I then mapped the above validator in Sitecore:

restricted-file-types-validator

After saving the above validator Item in Sitecore, I built the following validator class to check to see if a file exceeds a certain size:

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

using Sitecore.Form.Core.Validators;
using Sitecore.Form.Web.UI.Controls;

namespace Sitecore.Sandbox.Form.Core.Validators
{
    public class RestrictedFileSize : FormCustomValidator
    {
        public int MaxFileSize
        {
            get
            {
                int maxSize;
                if (int.TryParse(base.classAttributes["mimeTypesNotAllowed"], out maxSize))
                {
                    return maxSize;
                }

                return 0;
            }
            set
            {
                base.classAttributes["mimeTypesNotAllowed"] = value.ToString();
            }
        }

        public RestrictedFileSize()
        {
        }

        protected override bool OnServerValidate(string value)
        {
            FileUpload fileUpload = FindControl(ControlToValidate) as FileUpload;
            if (!fileUpload.HasFile)
            {
                return true;
            }

            return fileUpload.PostedFile.ContentLength <= MaxFileSize;
        }
    }
}

Just as we had done in the other validator, we grab the FileUpload from the form, and see if there is a file attached. If there is no file attached, we return “true”: we don’t want to say the field is invalid when the field is not required.

We then return whether the uploaded file is less than or equal to the specified maximum size in bytes — this is set on a parameter named MaxFileSize which gets injected into the MaxFileSize property of the validator instance.

I then registered the above validator in Sitecore:

restricted-file-size-validator

I then decided to create a custom WFFM field type for the purposes of mapping our validators above, so that we don’t enforce these restrictions on the “out of the box” WFFM “File Upload” field type:

restricted-file-upload-field-type

I then set the new field type on a file field I added to a test WFFM form:

set-custom-field-type

Let’s see how we did!

Let’s try to upload an executable that exceeds the maximum upload size limit:

big-exe-wffm-upload

As you can see both validators were triggered for this file:

big-exe-wffm-upload-errormessage

How about a JavaScript file? Let’s try to attach one:

js-wffm-upload

Nope, we can’t attach that file either:

js-wffm-upload-errormessage

How about an image that is larger than the size limit? Let’s try one:

big-image-wffm-upload

Nope, we can’t upload that either:

big-image-wffm-upload-errormessage

Let’s try an image that is under 100KB:

allowed-image-wffm-upload

Yes, we can attach that file since it’s not restricted, and the form did submit:

allowed-image-wffm-upload-no-errormessage

If you have any thoughts on this, or other ideas around preventing certain files from being attached to WFFM form submissions, please share in a comment.


Viewing all 112 articles
Browse latest View live