Home » Utilities
Category Archives: Utilities
Expand Tokens on Items Using a Sitecore PowerShell Extensions Toolbox Script
Last Wednesday I had the opportunity of presenting Sitecore PowerShell Extensions (SPE) at the Milwaukee Sitecore Meetup. During this presentation, I demonstrated how quickly and easily one can add, execute and reuse PowerShell scripts in SPE, and I did this using version 3.0 of SPE on Sitecore XP 8.
During one segment of the presentation, I shared how one can seamlessly add scripts to the SPE Toolbox — a repository of utility scripts if you will — and used the following script when showing this:
<# .NAME Expand tokens in all content items .SYNOPSIS Expand tokens in all fields in all content items .NOTES Mike Reynolds #> $items = Get-ChildItem -Path "master:\sitecore\content" -Recurse $items | ForEach-Object { $_.Fields.ReadAll() } $items | Expand-Token Close-Window
The script above grabs all descendant Items under /sitecore/content/; iterates over them to ensure all field values are available — the ReadAll() method on the FieldCollection instance will ensure values from fields on the Item’s template’s Standard Values Item are pulled in for processing; and sends in these Items into the Expand-Token commandlet which comes “out of the box” with SPE.
The script also closes the processing dialog.
I then saved the above script into my Toolbox library in my SPE module:
Let’s try this out. Let’s find some Items with tokens in some fields. It looks like the Home Item has some:
Here’s another Item that also has tokens:
Let’s go to the SPE Toolbox, and click on our Toolbox utility:
As you can see the tokens were expanded on the Home Item:
Tokens were also expanded on the descendant Item:
If you have any thoughts and/or suggestions on this, or have ideas for other SPE Toolbox scripts, please drop a comment.
If you would like to watch the Milwaukee Sitecore Meetup presentation where I showed the above — you’ll also get to see some epic Sitecore PowerShell Extensions stuff from Adam Brauer, Senior Product Engineer at Active Commerce, in this presentation as well — have a look below:
If you would like to see another example of adding a script to the SPE Toolbox, please see my previous post on this subject.
Until next time, have a scriptaculous day!
Restart the Sitecore Client and Server Using Custom Pipelines
Last week Michael West asked me about creating shortcuts to restart the Sitecore client and server via this tweet, and I was immediately curious myself on what was needed to accomplish this.
If you are unfamiliar with restarting the Sitecore client and server, these are options that are presented to Sitecore users — typically developers — after installing a package into Sitecore:
Until last week, I never understood how either of these checkboxes worked, and uncovered the following code during my research:
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:
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.
Expand Tokens on Sitecore Items Using a Custom Command in Sitecore PowerShell Extensions
During my Sitecore from the Command Line presentation at the Sitecore User Group – New England, I had shown attendees how they could go about adding a custom command into the Sitecore PowerShell Extensions module.
This blog post shows what I had presented — although the code in this post is an improved version over what I had presented at my talk. Many thanks to Sitecore MVP Adam Najmanowicz for helping me make this code better!
The following command will expand “out of the box” tokens in all fields of a supplied Sitecore item — check out Expand Tokens on Sitecore Items Using a Custom Command in Revolver where I discuss the problem commands like this address, and this article by Sitecore MVP Jens Mikkelsen which lists “out of the box” tokens available in Sitecore:
using System; using System.Management.Automation; using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Cognifide.PowerShell.PowerShellIntegrations.Commandlets; namespace CommandLineExtensions.PowerShell.Commandlets { [Cmdlet("Expand", "Token")] [OutputType(new[] { typeof(Item) })] public class ExpandTokenCommand : BaseCommand { private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer(); [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)] public Item Item { get; set; } protected override void ProcessRecord() { Item.Editing.BeginEdit(); try { TokenReplacer.ReplaceItem(Item); Item.Editing.EndEdit(); } catch (Exception ex) { Item.Editing.CancelEdit(); throw ex; } WriteItem(Item); } } }
The command above subclasses Cognifide.PowerShell.PowerShellIntegrations.Commandlets.BaseCommand — the base class for most (if not all) commands in Sitecore PowerShell Extensions.
An item is passed to the command via a parameter, and is magically set on the Item property of the command class instance.
The ValueFromPipeline parameter being set to “true” on the Item property’s Parameter attribute will allow for chaining of this command with others so that items can be fed into it via a pipe bridging the commands together in PowerShell.
An instance of the Sitecore.Data.MasterVariablesReplacer class — which is created by the GetMasterVariablesReplacer() method of the Sitecore.Configuration.Factory class based on the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — is used to expand tokens on the supplied Sitecore item after the item was flagged for editing.
Once tokens have been expanded on the item — or not in the event an exception is encountered — the item is written to the Results window via the WriteItem method which is defined in the BaseCommand class.
I then had to wire up the custom command via a patch configuration file:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <powershell> <commandlets> <add Name="Custom Commandlets" type="*, CommandLineExtensions" /> </commandlets> </powershell> </sitecore> </configuration>
Let’s take this custom command for a spin.
I created a bunch of test items, and set tokens in their fields. I then selected the following page at random for testing:
I opened up the Integrated Scripting Environment of Sitecore PowerShell Extensions, typed in the following PowerShell code, and executed by pressing Ctrl-E:
As you can see tokens were expanded on the Page One item:
How about expanding tokens on all descendants of the Home item? Let’s see an example of how we can do that.
I chose the following content item — a grandchild of the Home item — for testing:
I switched back over to the Integrated Scripting Environment, wrote the following code for testing — the Get-ChildItem command with the -r parameter (this means do this recursively) will grab all descendants of the Home item, and pipe each item in the result set into the Expand-Token command — and clicked the Execute button:
I then went back to the grandchild item of the Home page in the content tree, and saw that tokens were expanded in its fields:
If you have any thoughts or comments on this, or ideas for new commands in Sitecore PowerShell Extensions, please share in a comment.
Until next time, have a scriptolicious day!
Expand Tokens on Sitecore Items Using a Custom Command in Revolver
On September 18, 2013, I presented Sitecore from the Command Line at the Sitecore User Group – New England.
During my presentation, I gave an example of creating a custom command in Revolver — the first scripting platform for Sitecore built by Alistair Deneys — and thought I would write something up for those who had missed the presentation, or wanted to revisit what I had shown.
One thing that plagues some Sitecore developers — if you disagree please leave a comment — is not having a nice way to expand tokens on items when tokens are added to Standard Values after items had been created previously.
Newly added tokens “bleed” into preexisting items’ fields, and I’ve seen developers perform crazy feats of acrobatic gymnastics to expand them — writing a standalone web form to recursive crawl the content tree to expand these is such an example (take a look at Empower Your Content Authors to Expand Standard Values Tokens in the Sitecore Client where I offer an alternative way to expand tokens on content items).
The following custom Revolver command will expand tokens on a supplied Sitecore item, and help out on the front of expanding newly added tokens on preexisting items:
using System; using Sitecore.Configuration; using System.Linq; using Sitecore.Data; using Sitecore.Data.Items; using Revolver.Core; using Revolver.Core.Commands; namespace CommandLineExtensions.Revolver.Commands { public class ExpandTokensCommand : BaseCommand { private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer(); public override string Description() { return "Expand tokens on an item"; } public override HelpDetails Help() { HelpDetails details = new HelpDetails { Description = Description(), Usage = "<cmd> [path]" }; details.AddExample("<cmd>"); details.AddExample("<cmd> /item1/item2"); return details; } public override CommandResult Run(string[] args) { string path = string.Empty; if (args.Any()) { path = args.FirstOrDefault(); } using (new ContextSwitcher(Context, path)) { if (!Context.LastGoodPath.EndsWith(path, StringComparison.CurrentCultureIgnoreCase)) { return new CommandResult ( CommandStatus.Failure, string.Format("Failed to expand tokens on item {0}\nReason:\n\n An item does not exist at that location!", path) ); } CommandResult result; Item item = Context.CurrentItem; item.Editing.BeginEdit(); try { TokenReplacer.ReplaceItem(item); result = new CommandResult(CommandStatus.Success, string.Concat("Expanded tokens on item ", Context.LastGoodPath)); item.Editing.EndEdit(); } catch (Exception ex) { item.Editing.CancelEdit(); result = new CommandResult(CommandStatus.Failure, string.Format("Failed to expand tokens on item {0}\nReason:\n\n{1}", path, ex)); } return result; } } } }
Tokens are expanded using an instance of the Sitecore.Data.MasterVariablesReplacer class — you can roll your own, and wire it up in the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — which is provided by Sitecore.Configuration.Factory.GetMasterVariablesReplacer().
All custom commands in Revolver must implement the Revolver.Core.ICommand interface. I subclassed Revolver.Core.Commands.BaseCommand — which does implement this interface — since it seemed like the right thing to do given that all “out of the box” commands I saw in Revolver were subclassing it, and then implemented the Description(), Help() and Run() abstract methods.
I then had to bind the custom command to a new name — I chose “et” for “Expand Tokens”:
@echooff @stoponerror bind CommandLineExtensions.Revolver.Commands.ExpandTokensCommand,CommandLineExtensions et @echoon
Since it wouldn’t be efficient to type and run this bind script every time I want to use the “et” command, I added it into a startup script in the core database:
I then had to create a user script for the startup script to run. I chose the Everyone role here for demonstration purposes:
The above startup script will be invoked when Revolver is opened, and our custom command will be bound.
Let’s see all of the above in action.
I added some tokens in my home item:
I then opened up Revolver, navigated to /sitecore/content, and ran the custom command on the home item:
As you can see the tokens were expanded:
You might be thinking “that’s wonderful Mike — except now I have to navigate to every item in my content tree using Revolver, and then run this custom command on it”.
Well, I do have a solution for this: a custom script that grabs an item and all of its descendants using a Sitecore query, and passes them to the custom command to expand tokens:
@echooff @stoponerror if ($1$ = \$1\$) (exit (Missing required parameter path)) @echoon query -ns $1$/descendant-or-self::* et
I put this script in the core database, and named it “etr” for “Expand Tokens Recursively”:
I navigated to a descendant of /sitecore/content/home, and see that it has some unexpanded tokens on it:
I went back to Revolver, and ran the “etr” command on the home item:
As you can see tokens were expanded on the descendant item:
If you have any thoughts on this, or have ideas for other custom commands in Revolver, please share in a comment.
Make a Difference by Comparing Sitecore Items Across Different Databases
The other day I pondered whether anyone had ever built a tool in the Sitecore client to compare field values for the same item across different databases.
Instead of researching whether someone had built such a tool — bad, bad, bad Mike — I decided to build something to do just that — well, really leverage existing code used by Sitecore “out of the box”.
I thought it would be great if I could harness code used by the versions Diff tool — a tool that allows users to visually ascertain differences in fields of an item across different versions in the same database:
After digging around in Sitecore.Kernel.dll, I discovered I could reuse some logic from the versions Diff tool to accomplish this, and what follows showcases the fruit yielded from that research.
The first thing I built was a class — along with its interface — to return a collection of databases where an item resides:
using System.Collections.Generic; namespace Sitecore.Sandbox.Utilities.Gatherers.Base { public interface IDatabasesGatherer { IEnumerable<Sitecore.Data.Database> Gather(); } }
using System.Collections.Generic; using System.Linq; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Sandbox.Utilities.Gatherers.Base; using Sitecore.Configuration; namespace Sitecore.Sandbox.Utilities.Gatherers { public class ItemInDatabasesGatherer : IDatabasesGatherer { private ID ID { get; set; } private ItemInDatabasesGatherer(string id) : this(MainUtil.GetID(id)) { } private ItemInDatabasesGatherer(ID id) { SetID(id); } private void SetID(ID id) { AssertID(id); ID = id; } public IEnumerable<Sitecore.Data.Database> Gather() { return GetAllDatabases().Where(database => DoesDatabaseContainItemByID(database, ID)); } private static IEnumerable<Sitecore.Data.Database> GetAllDatabases() { return Factory.GetDatabases(); } private bool DoesDatabaseContainItemByID(Sitecore.Data.Database database, ID id) { return GetItem(database, id) != null; } private static Item GetItem(Sitecore.Data.Database database, ID id) { Assert.ArgumentNotNull(database, "database"); AssertID(id); return database.GetItem(id); } private static void AssertID(ID id) { Assert.ArgumentCondition(!ID.IsNullOrEmpty(id), "id", "ID must be set!"); } public static IDatabasesGatherer CreateNewItemInDatabasesGatherer(string id) { return new ItemInDatabasesGatherer(id); } public static IDatabasesGatherer CreateNewItemInDatabasesGatherer(ID id) { return new ItemInDatabasesGatherer(id); } } }
I then copied the xml from the versions Diff dialog — this lives in /sitecore/shell/Applications/Dialogs/Diff/Diff.xml — and replaced the versions Combobox dropdowns with my own for showing Sitecore database names:
<?xml version="1.0" encoding="utf-8" ?> <control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense"> <ItemDiff> <FormDialog Icon="Applications/16x16/window_view.png" Header="Database Compare" Text="Compare the same item in different databases. The differences are highlighted." CancelButton="false"> <CodeBeside Type="Sitecore.Sandbox.Shell.Applications.Dialogs.Diff.ItemDiff,Sitecore.Sandbox"/> <link href="/sitecore/shell/Applications/Dialogs/Diff/Diff.css" rel="stylesheet"/> <Stylesheet> .ie #GridContainer { padding: 4px; } .ff #GridContainer > * { padding: 4px; } .ff .scToolbutton, .ff .scToolbutton_Down, .ff .scToolbutton_Hover, .ff .scToolbutton_Down_Hover { height: 20px; float: left; } </Stylesheet> <AutoToolbar DataSource="/sitecore/content/Applications/Dialogs/Diff/Toolbar" def:placeholder="Toolbar"/> <GridPanel Columns="2" Width="100%" Height="100%" GridPanel.Height="100%"> <Combobox ID="DatabaseOneDropdown" Width="100%" GridPanel.Width="50%" GridPanel.Style="padding:0px 4px 4px 0px" Change="#"/> <Combobox ID="DatabaseTwoDropdown" Width="100%" GridPanel.Width="50%" GridPanel.Style="padding:0px 0px 4px 0px" Change="#"/> <Scrollbox ID="GridContainer" Padding="" Background="white" GridPanel.ColSpan="2" GridPanel.Height="100%"> <GridPanel ID="Grid" Width="100%" CellPadding="0" Fixed="true"></GridPanel> </Scrollbox> </GridPanel> </FormDialog> </ItemDiff> </control>
I saved the above xml in /sitecore/shell/Applications/Dialogs/ItemDiff/ItemDiff.xml.
With the help of the code-beside of the versions Diff tool — this lives in Sitecore.Shell.Applications.Dialogs.Diff.DiffForm — I built the code-beside for the xml control above:
using System; using System.Collections.Generic; using System.Linq; using System.Web.UI; using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.shell.Applications.Dialogs.Diff; using Sitecore.Text.Diff.View; using Sitecore.Web; using Sitecore.Web.UI.HtmlControls; using Sitecore.Web.UI.Sheer; using Sitecore.Web.UI.WebControls; using Sitecore.Sandbox.Utilities.Gatherers.Base; using Sitecore.Sandbox.Utilities.Gatherers; namespace Sitecore.Sandbox.Shell.Applications.Dialogs.Diff { public class ItemDiff : BaseForm { private const string IDKey = "id"; private const string OneColumnViewRegistry = "OneColumn"; private const string TwoColumnViewRegistry = "TwoColumn"; private const string ViewRegistryKey = "/Current_User/ItemDatabaseDiff/View"; protected Button Cancel; protected GridPanel Grid; protected Button OK; protected Combobox DatabaseOneDropdown; protected Combobox DatabaseTwoDropdown; private ID _ID; private ID ID { get { if (ID.IsNullOrEmpty(_ID)) { _ID = GetID(); } return _ID; } } private Database _DatabaseOne; private Database DatabaseOne { get { if (_DatabaseOne == null) { _DatabaseOne = GetDatabaseOne(); } return _DatabaseOne; } } private Database _DatabaseTwo; private Database DatabaseTwo { get { if (_DatabaseTwo == null) { _DatabaseTwo = GetDatabaseTwo(); } return _DatabaseTwo; } } private ID GetID() { return MainUtil.GetID(GetServerPropertySetIfApplicable(IDKey, IDKey), ID.Null); } private Database GetDatabaseOne() { return GetDatabase(DatabaseOneDropdown.SelectedItem.Value); } private Database GetDatabaseTwo() { return GetDatabase(DatabaseTwoDropdown.SelectedItem.Value); } private static Database GetDatabase(string databaseName) { if(!string.IsNullOrEmpty(databaseName)) { return Factory.GetDatabase(databaseName); } return null; } private static string GetServerPropertySetIfApplicable(string serverPropertyKey, string queryStringName, string defaultValue = null) { Assert.ArgumentNotNullOrEmpty(serverPropertyKey, "serverPropertyKey"); string value = GetServerProperty(serverPropertyKey); if(!string.IsNullOrEmpty(value)) { return value; } SetServerProperty(serverPropertyKey, GetQueryString(queryStringName, defaultValue)); return GetServerProperty(serverPropertyKey); } private static string GetServerProperty(string key) { Assert.ArgumentNotNullOrEmpty(key, "key"); return GetServerProperty<string>(key); } private static T GetServerProperty<T>(string key) where T : class { Assert.ArgumentNotNullOrEmpty(key, "key"); return Context.ClientPage.ServerProperties[key] as T; } private static void SetServerProperty(string key, object value) { Assert.ArgumentNotNullOrEmpty(key, "key"); Context.ClientPage.ServerProperties[key] = value; } private static string GetQueryString(string name, string defaultValue = null) { Assert.ArgumentNotNullOrEmpty(name, "name"); if(!string.IsNullOrEmpty(defaultValue)) { return WebUtil.GetQueryString(name, defaultValue); } return WebUtil.GetQueryString(name); } private void Compare() { Compare(GetDiffView(), Grid, GetItemOne(), GetItemTwo()); } private static void Compare(DiffView diffView, GridPanel gridPanel, Item itemOne, Item itemTwo) { Assert.ArgumentNotNull(diffView, "diffView"); Assert.ArgumentNotNull(gridPanel, "gridPanel"); Assert.ArgumentNotNull(itemOne, "itemOne"); Assert.ArgumentNotNull(itemTwo, "itemTwo"); diffView.Compare(gridPanel, itemOne, itemTwo, string.Empty); } private static DiffView GetDiffView() { if (IsOneColumnSelected()) { return new OneColumnDiffView(); } return new TwoCoumnsDiffView(); } private Item GetItemOne() { Assert.IsNotNull(DatabaseOne, "DatabaseOne must be set!"); return DatabaseOne.Items[ID]; } private Item GetItemTwo() { Assert.IsNotNull(DatabaseOne, "DatabaseTwo must be set!"); return DatabaseTwo.Items[ID]; } private static void OnCancel(object sender, EventArgs e) { Assert.ArgumentNotNull(sender, "sender"); Assert.ArgumentNotNull(e, "e"); Context.ClientPage.ClientResponse.CloseWindow(); } protected override void OnLoad(EventArgs e) { Assert.ArgumentNotNull(e, "e"); base.OnLoad(e); OK.OnClick += new EventHandler(OnOK); Cancel.OnClick += new EventHandler(OnCancel); DatabaseOneDropdown.OnChange += new EventHandler(OnUpdate); DatabaseTwoDropdown.OnChange += new EventHandler(OnUpdate); } private static void OnOK(object sender, EventArgs e) { Assert.ArgumentNotNull(sender, "sender"); Assert.ArgumentNotNull(e, "e"); Context.ClientPage.ClientResponse.CloseWindow(); } protected override void OnPreRender(EventArgs e) { Assert.ArgumentNotNull(e, "e"); base.OnPreRender(e); if (!Context.ClientPage.IsEvent) { PopuplateDatabaseDropdowns(); Compare(); UpdateButtons(); } } private void PopuplateDatabaseDropdowns() { IDatabasesGatherer IDatabasesGatherer = ItemInDatabasesGatherer.CreateNewItemInDatabasesGatherer(ID); PopuplateDatabaseDropdowns(IDatabasesGatherer.Gather()); } private void PopuplateDatabaseDropdowns(IEnumerable<Database> databases) { PopuplateDatabaseDropdown(DatabaseOneDropdown, databases, Context.ContentDatabase); PopuplateDatabaseDropdown(DatabaseTwoDropdown, databases, Context.ContentDatabase); } private static void PopuplateDatabaseDropdown(Combobox databaseDropdown, IEnumerable<Database> databases, Database selectedDatabase) { Assert.ArgumentNotNull(databaseDropdown, "databaseDropdown"); Assert.ArgumentNotNull(databases, "databases"); foreach (Database database in databases) { databaseDropdown.Controls.Add ( new ListItem { ID = Sitecore.Web.UI.HtmlControls.Control.GetUniqueID("ListItem"), Header = database.Name, Value = database.Name, Selected = string.Equals(database.Name, selectedDatabase.Name) } ); } } private void OnUpdate(object sender, EventArgs e) { Assert.ArgumentNotNull(sender, "sender"); Assert.ArgumentNotNull(e, "e"); Refresh(); } private void Refresh() { Grid.Controls.Clear(); Compare(); Context.ClientPage.ClientResponse.SetOuterHtml("Grid", Grid); } protected void ShowOneColumn() { SetRegistryString(ViewRegistryKey, OneColumnViewRegistry); UpdateButtons(); Refresh(); } protected void ShowTwoColumns() { SetRegistryString(ViewRegistryKey, TwoColumnViewRegistry); UpdateButtons(); Refresh(); } private static void UpdateButtons() { bool isOneColumnSelected = IsOneColumnSelected(); SetToolButtonDown("OneColumn", isOneColumnSelected); SetToolButtonDown("TwoColumn", !isOneColumnSelected); } private static bool IsOneColumnSelected() { return string.Equals(GetRegistryString(ViewRegistryKey, OneColumnViewRegistry), OneColumnViewRegistry); } private static void SetToolButtonDown(string controlID, bool isDown) { Assert.ArgumentNotNullOrEmpty(controlID, "controlID"); Toolbutton toolbutton = FindClientPageControl<Toolbutton>(controlID); toolbutton.Down = isDown; } private static T FindClientPageControl<T>(string controlID) where T : System.Web.UI.Control { Assert.ArgumentNotNullOrEmpty(controlID, "controlID"); T control = Context.ClientPage.FindControl(controlID) as T; Assert.IsNotNull(control, typeof(T)); return control; } private static string GetRegistryString(string key, string defaultValue = null) { Assert.ArgumentNotNullOrEmpty(key, "key"); if(!string.IsNullOrEmpty(defaultValue)) { return Sitecore.Web.UI.HtmlControls.Registry.GetString(key, defaultValue); } return Sitecore.Web.UI.HtmlControls.Registry.GetString(key); } private static void SetRegistryString(string key, string value) { Assert.ArgumentNotNullOrEmpty(key, "key"); Sitecore.Web.UI.HtmlControls.Registry.SetString(key, value); } } }
The code-beside file above populates the two database dropdowns with the names of the databases where the Item is found, and selects the current content database on both dropdowns when the dialog is first launched.
Users have the ability to toggle between one and two column layouts — just as is offered by the versions Diff tool — and can compare field values on the item across any database where the item is found — the true magic occurs in the instance of the Sitecore.Text.Diff.View.DiffView class.
Now that we have a dialog form, we need a way to launch it:
using System; using System.Collections.Generic; using System.Linq; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Shell.Framework.Commands; using Sitecore.Text; using Sitecore.Web.UI.Sheer; using Sitecore.Sandbox.Utilities.Gatherers.Base; using Sitecore.Sandbox.Utilities.Gatherers; namespace Sitecore.Sandbox.Commands { public class LaunchDatabaseCompare : Command { public override void Execute(CommandContext commandContext) { SheerResponse.CheckModified(false); SheerResponse.ShowModalDialog(GetDialogUrl(commandContext)); } private static string GetDialogUrl(CommandContext commandContext) { return GetDialogUrl(GetItem(commandContext).ID); } private static string GetDialogUrl(ID id) { Assert.ArgumentCondition(!ID.IsNullOrEmpty(id), "id", "ID must be set!"); UrlString urlString = new UrlString(UIUtil.GetUri("control:ItemDiff")); urlString.Append("id", id.ToString()); return urlString.ToString(); } public override CommandState QueryState(CommandContext commandContext) { IDatabasesGatherer databasesGatherer = ItemInDatabasesGatherer.CreateNewItemInDatabasesGatherer(GetItem(commandContext).ID); if (databasesGatherer.Gather().Count() > 1) { return CommandState.Enabled; } return CommandState.Disabled; } private static Item GetItem(CommandContext commandContext) { Assert.ArgumentNotNull(commandContext, "commandContext"); Assert.ArgumentNotNull(commandContext.Items, "commandContext.Items"); return commandContext.Items.FirstOrDefault(); } } }
The above command launches our ItemDiff dialog, and passes the ID of the selected item to it.
If the item is only found in one database — this will be the current content database — the command is disabled. What would be the point of comparing the item in the same database?
I then registered this command in a patch include configuration file:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <sitecore> <commands> <command name="item:launchdatabasecompare" type="Sitecore.Sandbox.Commands.LaunchDatabaseCompare,Sitecore.Sandbox"/> </commands> </sitecore> </configuration>
Now that we have our command ready to go, we need to lock and load this command in the Sitecore client. I added a button for our new command in the Operations chunk under the Home ribbon:
Time for some fun.
I created a new item for testing:
I published this item, and made some changes to it:
I clicked the Database Compare button to launch our dialog form:
As expected, we see differences in this item across the master and web databases:
Here are those differences in the two column layout:
One thing I might consider adding in the future is supporting comparisons of different versions of items across databases. The above solution is limited in only allowing users to compare the latest version of the Item in each database.
If you can think of anything else that could be added to this to make it better, please drop a comment.
Where Is This Field Defined? Add ‘Goto Template’ Links for Fields in the Sitecore Content Editor
About a month ago, I read this answer on Stack Overflow by Sitecore MVP Dan Solovay, and thought to myself “what could I do with a custom EditorFormatter that might be useful?”
Today, I came up with an idea that might be useful, especially when working with many levels of nested base templates: having a ‘Goto Template’ link — or button depending on your naming preference, although I will refer to these as links throughout this post since they are hyperlinks — for each field that, when clicked, will bring you to the Sitecore template where the field is defined.
I first defined a class to manage the display state of our ‘Goto Template’ links in the Content Editor:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Web.UI.HtmlControls; namespace Sitecore.Sandbox.Utilities.ClientSettings { public class GotoTemplateLinksSettings { private const string RegistrySettingKey = "/Current_User/Content Editor/Goto Template Links"; private const string RegistrySettingOnValue = "on"; private static volatile GotoTemplateLinksSettings current; private static object lockObject = new Object(); public static GotoTemplateLinksSettings Current { get { if (current == null) { lock (lockObject) { if (current == null) current = new GotoTemplateLinksSettings(); } } return current; } } private GotoTemplateLinksSettings() { } public bool IsOn() { return Registry.GetString(RegistrySettingKey) == RegistrySettingOnValue; } public void TurnOn() { Registry.SetString(RegistrySettingKey, RegistrySettingOnValue); } public void TurnOff() { Registry.SetString(RegistrySettingKey, string.Empty); } } }
I decided to make the above class be a Singleton — there should only be one central place where the display state of our links is toggled.
I created a subclass of Sitecore.Shell.Applications.ContentEditor.EditorFormatter, and overrode the RenderField(Control, Editor.Field, bool) method to embed additional logic to render a ‘Goto Template’ link for each field in the Content Editor:
using System.Web.UI; using Sitecore; using Sitecore.Data.Fields; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Shell.Applications.ContentEditor; using Sitecore.Shell.Applications.ContentManager; using Sitecore.Sandbox.Utilities.ClientSettings; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor { public class GotoTemplateEditorFormatter : EditorFormatter { public override void RenderField(Control parent, Editor.Field field, bool readOnly) { Assert.ArgumentNotNull(parent, "parent"); Assert.ArgumentNotNull(field, "field"); Field itemField = field.ItemField; Item fieldType = GetFieldType(itemField); if (fieldType != null) { if (!itemField.CanWrite) { readOnly = true; } RenderMarkerBegin(parent, field.ControlID); RenderMenuButtons(parent, field, fieldType, readOnly); RenderLabel(parent, field, fieldType, readOnly); AddGotoTemplateLinkIfCanView(parent, field); RenderField(parent, field, fieldType, readOnly); RenderMarkerEnd(parent); } } public void AddGotoTemplateLinkIfCanView(Control parent, Editor.Field field) { if (CanViewGotoTemplateLink()) { AddGotoTemplateLink(parent, field); } } private static bool CanViewGotoTemplateLink() { return IsGotoTemplateLinksOn(); } private static bool IsGotoTemplateLinksOn() { return GotoTemplateLinksSettings.Current.IsOn(); } public void AddGotoTemplateLink(Control parent, Editor.Field field) { Assert.ArgumentNotNull(parent, "parent"); Assert.ArgumentNotNull(field, "field"); AddLiteralControl(parent, CreateGotoTemplateLink(field)); } private static string CreateGotoTemplateLink(Editor.Field field) { Assert.ArgumentNotNull(field, "field"); return string.Format("<a title=\"Navigate to the template where this field is defined.\" style=\"float: right;position:absolute;margin-top:-20px;right:15px;\" href=\"#\" onclick=\"{0}\">{1}</a>", CreateGotoTemplateJavascript(field), CreateGotoTemplateLinkText()); } private static string CreateGotoTemplateJavascript(Editor.Field field) { Assert.ArgumentNotNull(field, "field"); return string.Format("javascript:scForm.postRequest('', '', '','item:load(id={0})');return false;", field.TemplateField.Template.ID); } private static string CreateGotoTemplateLinkText() { return "<img style=\"border: 0;\" src=\"/~/icon/Applications/16x16/information2.png\" width=\"16\" height=\"16\" />"; } } }
‘Goto Template’ links are only rendered to the Sitecore Client when the display state for showing them is turned on.
Plus, each ‘Goto Template’ link is locked and loaded to invoke the item load command to navigate to the template item where the field is defined.
As highlighted by Dan in his Stack Overflow answer above, I created a new Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor pipeline processor, and hooked in an instance of the GotoTemplateEditorFormatter class defined above:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Diagnostics; using Sitecore.Shell.Applications.ContentEditor; using Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor { public class RenderStandardContentEditor { public void Process(RenderContentEditorArgs args) { Assert.ArgumentNotNull(args, "args"); args.EditorFormatter = CreateNewGotoTemplateEditorFormatter(args); args.EditorFormatter.RenderSections(args.Parent, args.Sections, args.ReadOnly); } private static EditorFormatter CreateNewGotoTemplateEditorFormatter(RenderContentEditorArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.EditorFormatter, "args.EditorFormatter"); return new GotoTemplateEditorFormatter { Arguments = args.EditorFormatter.Arguments, IsFieldEditor = args.EditorFormatter.IsFieldEditor }; } } }
Now we need a way to toggle the display state of our ‘Goto Template’ links. I decided to create a command to turn this state on and off:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Shell; using Sitecore.Shell.Framework.Commands; using Sitecore.Web.UI.HtmlControls; using Sitecore.Sandbox.Utilities.ClientSettings; namespace Sitecore.Sandbox.Commands { public class ToggleGotoTemplateLinks : Command { public override void Execute(CommandContext commandContext) { ToggleGotoTemplateLinksOn(); Refresh(commandContext); } private static void ToggleGotoTemplateLinksOn() { GotoTemplateLinksSettings gotoTemplateLinksSettings = GotoTemplateLinksSettings.Current; if (!gotoTemplateLinksSettings.IsOn()) { gotoTemplateLinksSettings.TurnOn(); } else { gotoTemplateLinksSettings.TurnOff(); } } private static void Refresh(CommandContext commandContext) { Refresh(GetItem(commandContext)); } private static void Refresh(Item item) { Assert.ArgumentNotNull(item, "item"); Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", item.ID), 1); } private static Item GetItem(CommandContext commandContext) { Assert.ArgumentNotNull(commandContext, "commandContext"); return commandContext.Items.FirstOrDefault(); } public override CommandState QueryState(CommandContext context) { if (!GotoTemplateLinksSettings.Current.IsOn()) { return CommandState.Enabled; } return CommandState.Down; } } }
I registered the pipeline processor defined above coupled with the toggle command in a patch include configuration file:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <commands> <command name="contenteditor:togglegototemplatelinks" type="Sitecore.Sandbox.Commands.ToggleGotoTemplateLinks, Sitecore.Sandbox"/> </commands> <pipelines> <renderContentEditor> <processor patch:instead="*[@type='Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderStandardContentEditor, Sitecore.Client']" type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderStandardContentEditor, Sitecore.Sandbox"/> </renderContentEditor> </pipelines> </sitecore> </configuration>
I then added a toggle checkbox to the View ribbon, and wired up the ToggleGotoTemplateLinks command to it:
When the ‘Goto Template Links’ checkbox is checked, ‘Goto Template’ links are displayed for each field in the Content Editor:
When unchecked, the ‘Goto Template’ links are not rendered:
Let’s try it out.
Let’s click one of these ‘Goto Template’ links and see what it does, or where it takes us:
It brought us to the template where the Title field is defined:
Let’s try another. How about the ‘Created By’ field?
Its link brought us to its template:
Without a doubt, the functionality above would be useful to developers and advanced users.
I’ve been trying to figure out other potential uses for other subclasses of EditorFormatter. Can you think of other ways we could leverage a custom EditorFormatter, especially one for non-technical Sitecore users? If you have any ideas, please drop a comment. 🙂
Take the Field By Replicating Sitecore Field Values
I pondered the other day whether anyone had ever erroneously put content into fields on the wrong Sitecore item, only to discover they had erred after laboring away for an extended period of time — imagine the ensuing frustration after realizing such a blunder.
You might think that this isn’t a big deal — why not just rename the item to be the name of the item you were supposed to be putting content into in the first place?
Well, things might not be that simple.
What if the item already had content in it before? What do you do?
This hypothetical — or fictitious — scenario got the creative juices flowing. Why not create new item context menu options — check out part 1 and part 2 of my post discussing how one would go about augmenting the item context menu, and also my last post showing how one can delete sitecore items using a deletion basket which is serves as another example of adding to the item context menu — that give copy and paste functionality for field values?
The cornerstone of my idea comes from functionality that comes with Sitecore “out of the box”. You have the option to cut, copy and paste items:
I find these three menu options to be indispensable. I frequently use them throughout the day when developing new features in Sitecore, and have also seen content authors use these to do their work.
The only problem with these is they don’t work at the field level, ergo the reason for this post: to showcase my efforts in building copy and paste utilities that work at the field level.
I first had to come up with a way to save field values. The following interface serves as the definition of objects that save information associated with a key:
namespace Sitecore.Sandbox.Utilities.Storage.Base { public interface IRepository<TKey, TValue> { bool Contains(TKey key); TValue this[TKey key] { get; set; } void Put(TKey key, TValue value); void Remove(TKey key); void Clear(); TValue Get(TKey key); } }
For my copy and paste utilities, I decided I would store them in session. That steered me into building the following session repository class:
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.Text; using System.Web; using System.Web.SessionState; using Sitecore.Diagnostics; using Sitecore.Sandbox.Utilities.Storage.Base; namespace Sitecore.Sandbox.Utilities.Storage { public class SessionRepository : IRepository<string, object> { private HttpSessionStateBase Session { get; set; } public object this[string key] { get { return Get(key); } set { Put(key, value); } } private SessionRepository(HttpSessionState session) : this(CreateNewHttpSessionStateWrapper(session)) { } private SessionRepository(HttpSessionStateBase session) { SetSession(session); } private void SetSession(HttpSessionStateBase session) { Assert.ArgumentNotNull(session, "session"); Session = session; } public bool Contains(string key) { return Session[key] != null; } public void Put(string key, object value) { Assert.ArgumentNotNullOrEmpty(key, "key"); Assert.ArgumentCondition(IsSerializable(value), "value", "value must be serializable!"); Session[key] = value; } private static bool IsSerializable(object instance) { Assert.ArgumentNotNull(instance, "instance"); return instance.GetType().IsSerializable; } public void Remove(string key) { Session.Remove(key); } public void Clear() { Session.Clear(); } public object Get(string key) { return Session[key]; } private static HttpSessionStateWrapper CreateNewHttpSessionStateWrapper(HttpSessionState session) { Assert.ArgumentNotNull(session, "session"); return new HttpSessionStateWrapper(session); } public static IRepository<string, object> CreateNewSessionRepository(HttpSessionState session) { return new SessionRepository(session); } public static IRepository<string, object> CreateNewSessionRepository(HttpSessionStateBase session) { return new SessionRepository(session); } } }
If you’ve read some of my previous posts, you must have ascertained how I favor composition over inheritance — check out this article that discusses this subject — and created another utility object for storing string values — instances of this class delegate to other repository objects that save generic objects (an instance of the session repository class above is an example of such an object):
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.Text; using System.Web; using System.Web.SessionState; using Sitecore.Diagnostics; using Sitecore.Sandbox.Utilities.Storage.Base; namespace Sitecore.Sandbox.Utilities.Storage { public class StringRepository : IRepository<string, string> { private IRepository<string, object> InnerRepository { get; set; } public string this[string key] { get { return Get(key); } set { Put(key, value); } } private StringRepository(IRepository<string, object> innerRepository) { SetInnerRepository(innerRepository); } private void SetInnerRepository(IRepository<string, object> innerRepository) { Assert.ArgumentNotNull(innerRepository, "innerRepository"); InnerRepository = innerRepository; } public bool Contains(string key) { return InnerRepository.Contains(key); } public void Put(string key, string value) { Assert.ArgumentNotNullOrEmpty(key, "key"); InnerRepository.Put(key, value); } public void Remove(string key) { InnerRepository.Remove(key); } public void Clear() { InnerRepository.Clear(); } public string Get(string key) { return InnerRepository.Get(key) as string; } public static IRepository<string, string> CreateNewStringRepository(IRepository<string, object> innerRepository) { return new StringRepository(innerRepository); } } }
I then built another — yes one more — repository class that uses instances of Sitecore.Data.ID as keys:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Data; using Sitecore.Data.Fields; using Sitecore.Diagnostics; using Sitecore.Sandbox.Utilities.Storage.Base; namespace Sitecore.Sandbox.Utilities.Storage { class IDValueRepository : IRepository<ID, string> { private IRepository<string, string> InnerRepository { get; set; } public string this[ID key] { get { return Get(key); } set { Put(key, value); } } private IDValueRepository(IRepository<string, string> innerRepository) { SetInnerRepository(innerRepository); } private void SetInnerRepository(IRepository<string, string> innerRepository) { Assert.ArgumentNotNull(innerRepository, "innerRepository"); InnerRepository = innerRepository; } public bool Contains(ID key) { return InnerRepository.Contains(GetInnerRepositoryKey(key)); } public void Put(ID key, string value) { InnerRepository.Put(GetInnerRepositoryKey(key), value); } public void Remove(ID key) { InnerRepository.Remove(GetInnerRepositoryKey(key)); } public void Clear() { InnerRepository.Clear(); } public string Get(ID key) { return InnerRepository.Get(GetInnerRepositoryKey(key)); } private static string GetInnerRepositoryKey(ID key) { AssertKey(key); return key.ToString(); } private static void AssertKey(ID key) { Assert.ArgumentNotNull(key, "key"); Assert.ArgumentCondition(!ID.IsNullOrEmpty(key), "key", "key must be set!"); } public static IRepository<ID, string> CreateNewIDValueRepository(IRepository<string, string> innerRepository) { return new IDValueRepository(innerRepository); } } }
Instances of the above class delegate down to repository objects that save strings using strings as keys.
Now that I have an unwieldy arsenal of repository utility classes — I went a little bananas on creating the utility classes above — I figured having a factory class as a central place to instantiate these repository objects would aid in keeping things organized:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Data.Fields; using System.Web; using System.Web.SessionState; using Sitecore.Data; namespace Sitecore.Sandbox.Utilities.Storage.Base { public interface IStorageFactory { IRepository<string, object> CreateNewSessionRepository(HttpSessionState session); IRepository<string, object> CreateNewSessionRepository(HttpSessionStateBase session); IRepository<string, string> CreateNewStringRepository(IRepository<string, object> innerRepository); IRepository<ID, string> CreateNewIDValueRepository(HttpSessionState session); IRepository<ID, string> CreateNewIDValueRepository(IRepository<string, string> innerRepository); } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Web.SessionState; using Sitecore.Data; using Sitecore.Data.Fields; using Sitecore.Sandbox.Utilities.Storage.Base; namespace Sitecore.Sandbox.Utilities.Storage { public class StorageFactory : IStorageFactory { private StorageFactory() { } public IRepository<string, object> CreateNewSessionRepository(HttpSessionState session) { return SessionRepository.CreateNewSessionRepository(session); } public IRepository<string, object> CreateNewSessionRepository(HttpSessionStateBase session) { return SessionRepository.CreateNewSessionRepository(session); } public IRepository<string, string> CreateNewStringRepository(IRepository<string, object> innerRepository) { return StringRepository.CreateNewStringRepository(innerRepository); } public IRepository<ID, string> CreateNewIDValueRepository(HttpSessionState session) { return CreateNewIDValueRepository(CreateNewStringRepository(CreateNewSessionRepository(session))); } public IRepository<ID, string> CreateNewIDValueRepository(IRepository<string, string> innerRepository) { return IDValueRepository.CreateNewIDValueRepository(innerRepository); } public static IStorageFactory CreateNewStorageFactory() { return new StorageFactory(); } } }
Let’s make these utility repository classes earn their keep. It’s time to build a copy command:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using Sitecore.Collections; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Data.Fields; using Sitecore.Diagnostics; using Sitecore.Shell.Framework.Commands; using Sitecore.Sandbox.Utilities.Storage; using Sitecore.Sandbox.Utilities.Storage.Base; namespace Sitecore.Sandbox.Commands { public class CopyFieldValues : Command { private static readonly IStorageFactory Factory = StorageFactory.CreateNewStorageFactory(); private static readonly IRepository<ID, string> FieldValueRepository = CreateNewIDValueRepository(); public override void Execute(CommandContext commandContext) { Assert.ArgumentNotNull(commandContext, "commandContext"); StoreFieldValues(GetItem(commandContext)); } private static void StoreFieldValues(Item item) { if (item != null) { item.Fields.ReadAll(); StoreFieldValues(item.Fields); } } private static void StoreFieldValues(IEnumerable<Field> fields) { Assert.ArgumentNotNull(fields, "fields"); foreach (Field field in fields) { FieldValueRepository.Put(field.ID, field.Value); } } public override CommandState QueryState(CommandContext commandContext) { Assert.ArgumentNotNull(commandContext, "commandContext"); if (GetItem(commandContext).Appearance.ReadOnly) { return CommandState.Disabled; } return base.QueryState(commandContext); } private static Item GetItem(CommandContext commandContext) { Assert.ArgumentNotNull(commandContext, "commandContext"); Assert.ArgumentNotNull(commandContext.Items, "commandContext.Items"); Assert.ArgumentCondition(commandContext.Items.Count() > 0, "commandContext.Items", "There must be at least one item in the array!"); return commandContext.Items.FirstOrDefault(); } private static IRepository<ID, string> CreateNewIDValueRepository() { return Factory.CreateNewIDValueRepository(HttpContext.Current.Session); } } }
The above command iterates over all fields on the currently select item in the content tree, and saves their values using an instance of the IDValueRepository class.
What good is a copy command without a paste? The following paste command complements the copy command defined above:
using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Text; using System.Web; using Sitecore.Collections; using Sitecore.Data; using Sitecore.Data.Fields; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Shell.Framework.Commands; using Sitecore.Web.UI.Sheer; using Sitecore.Sandbox.Utilities.Storage; using Sitecore.Sandbox.Utilities.Storage.Base; namespace Sitecore.Sandbox.Commands { public class PasteFieldValues : Command { private static readonly IStorageFactory Factory = StorageFactory.CreateNewStorageFactory(); private static readonly IRepository<ID, string> FieldValueRepository = CreateNewIDValueRepository(); public override void Execute(CommandContext commandContext) { Assert.ArgumentNotNull(commandContext, "commandContext"); PasteValuesIfApplicable(commandContext); } private void PasteValuesIfApplicable(CommandContext commandContext) { Item item = GetItem(commandContext); if (item == null) { return; } PasteValuesIfApplicable(item); } private void PasteValuesIfApplicable(Item item) { Assert.ArgumentNotNull(item, "item"); if (DoesFieldsHaveValues(item)) { ConfirmThenPaste(item); } else { PasteValues(item); } } private static bool DoesFieldsHaveValues(Item item) { Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNull(item.Fields, "item.Fields"); foreach (Field field in item.Fields) { if (!string.IsNullOrEmpty(field.Value)) { return true; } } return false; } private void ConfirmThenPaste(Item item) { NameValueCollection parameters = new NameValueCollection(); parameters["items"] = SerializeItems(new Item[] { item }); Context.ClientPage.Start(this, "ConfirmAndPaste", new ClientPipelineArgs { Parameters = parameters }); } private void ConfirmAndPaste(ClientPipelineArgs args) { ShowConfirmationDialogIfApplicable(args); PasteValuesIfConfirmed(args); } private void ShowConfirmationDialogIfApplicable(ClientPipelineArgs args) { if (!args.IsPostBack) { Context.ClientPage.ClientResponse.YesNoCancel("Some fields are not empty! Are you sure you want to paste field values into this item?", "200", "200"); args.WaitForPostBack(); } } private void PasteValuesIfConfirmed(ClientPipelineArgs args) { bool canPaste = args.IsPostBack && args.Result == "yes"; if (canPaste) { Item item = DeserializeItems(args.Parameters["items"]).FirstOrDefault(); PasteValues(item); } } private static void PasteValues(Item item) { if (item != null) { item.Editing.BeginEdit(); item.Fields.ReadAll(); PasteValues(item.Fields); item.Editing.EndEdit(); } } private static void PasteValues(IEnumerable<Field> fields) { Assert.ArgumentNotNull(fields, "fields"); foreach (Field field in fields) { string value = FieldValueRepository.Get(field.ID); if (!string.IsNullOrEmpty(value)) { field.Value = value; } FieldValueRepository.Remove(field.ID); } } public override CommandState QueryState(CommandContext commandContext) { Assert.ArgumentNotNull(commandContext, "commandContext"); if (GetItem(commandContext).Appearance.ReadOnly) { return CommandState.Disabled; } return base.QueryState(commandContext); } private static Item GetItem(CommandContext commandContext) { Assert.ArgumentNotNull(commandContext, "commandContext"); Assert.ArgumentNotNull(commandContext.Items, "commandContext.Items"); Assert.ArgumentCondition(commandContext.Items.Count() > 0, "commandContext.Items", "There must be at least one item in the array!"); return commandContext.Items.FirstOrDefault(); } private static IRepository<ID, string> CreateNewIDValueRepository() { return Factory.CreateNewIDValueRepository(HttpContext.Current.Session); } } }
The paste command determines if the target item has any fields with content in them — a confirmation dialog box is displayed if any of the fields are not empty — and pastes values into fields if they are present on the item.
Plus, once a field value is retrieved from the IDValueRepository instance, the above command removes it. I couldn’t think of a good reason why these should linger in session after they are pasted. If you can of a reason why they should persist in session, please leave a comment.
I registered the copy and paste commands above into a patch include file:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <sitecore> <commands> <command name="item:copyfieldvalues" type="Sitecore.Sandbox.Commands.CopyFieldValues,Sitecore.Sandbox"/> <command name="item:pastefieldvalues" type="Sitecore.Sandbox.Commands.PasteFieldValues,Sitecore.Sandbox"/> </commands> </sitecore> </configuration>
I created context menu options for these in the Core database:
Let’s take all of the above for a spin.
I created an item with some content:
I then created another item with less content:
I right-clicked to launch the item context menu, and clicked the ‘Copy Field Values’ option:
I then navigated to the second item I created in the content tree, right-clicked, and selected the ‘Paste Field Values’ option:
By now, I had put my feet up on my desk thinking it was smooth sailing from this point on, only to be impeded by an intrusive confirmation box ;):
I clicked ‘Yes’, and saw the following thereafter:
In retrospect, it probably would have made more sense to omit standard fields from being copied, albeit I will leave that for another day.
Until next time, have a Sitecoretastic day! 🙂
Prevent Sitecore Users from Using Common Dictionary Words in Passwords
Lately, enhancing security measures in Sitecore have been on my mind. One idea that came to mind today was finding a way to prevent password cracking — a scenario where a script tries to ascertain a user’s password by guessing over and over again what a user’s password might be, by supplying common words from a data store, or dictionary in the password textbox on the Sitecore login page.
As a way to prevent users from using common words in their Sitecore passwords, I decided to build a custom System.Web.Security.MembershipProvider — the interface (not a .NET interface but an abstract class) used by Sitecore out of the box for user management. Before we dive into that code, we need a dictionary of some sort.
I decided to use a list of fruits — all for the purposes of keeping this post simple — that I found on a Wikipedia page. People aren’t kidding when they say you can virtually find everything on Wikipedia, and no doubt all content on Wikipedia is authoritative — just ask any university professor. 😉
I copied some of the fruits on that Wikipedia page into a patch include config file — albeit it would make more sense to put these into a database, or perhaps into Sitecore if doing such a thing in a real-world solution. I am not doing this here for the sake of brevity.
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <dictionaryWords> <word>Apple</word> <word>Apricot</word> <word>Avocado</word> <word>Banana</word> <word>Breadfruit</word> <word>Bilberry</word> <word>Blackberry</word> <word>Blackcurrant</word> <word>Blueberry</word> <word>Currant</word> <word>Cherry</word> <word>Cherimoya</word> <word>Clementine</word> <word>Cloudberry</word> <word>Coconut</word> <word>Date</word> <word>Damson</word> <word>Dragonfruit</word> <word>Durian</word> <word>Eggplant</word> <word>Elderberry</word> <word>Feijoa</word> <word>Fig</word> <word>Gooseberry</word> <word>Grape</word> <word>Grapefruit</word> <word>Guava</word> <word>Huckleberry</word> <word>Honeydew</word> <word>Jackfruit</word> <word>Jettamelon</word> <word>Jambul</word> <word>Jujube</word> <word>Kiwi fruit</word> <word>Kumquat</word> <word>Legume</word> <word>Lemon</word> <word>Lime</word> <word>Loquat</word> <word>Lychee</word> <word>Mandarine</word> <word>Mango</word> <word>Melon</word> </dictionaryWords> </sitecore> </configuration>
I decided to reuse a utility class I built for my post on expanding Standard Values tokens.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Diagnostics; using Sitecore.Sandbox.Utilities.StringUtilities.Base; using Sitecore.Sandbox.Utilities.StringUtilities.DTO; namespace Sitecore.Sandbox.Utilities.StringUtilities { public class StringSubstringsChecker : SubstringsChecker<string> { private bool IgnoreCase { get; set; } private StringSubstringsChecker(IEnumerable<string> substrings) : this(null, substrings, false) { } private StringSubstringsChecker(IEnumerable<string> substrings, bool ignoreCase) : this(null, substrings, ignoreCase) { } private StringSubstringsChecker(string source, IEnumerable<string> substrings, bool ignoreCase) : base(source) { SetSubstrings(substrings); SetIgnoreCase(ignoreCase); } private void SetSubstrings(IEnumerable<string> substrings) { AssertSubstrings(substrings); Substrings = substrings; } private static void AssertSubstrings(IEnumerable<string> substrings) { Assert.ArgumentNotNull(substrings, "substrings"); Assert.ArgumentCondition(substrings.Any(), "substrings", "substrings must contain as at least one string!"); } private void SetIgnoreCase(bool ignoreCase) { IgnoreCase = ignoreCase; } protected override bool CanDoCheck() { return !string.IsNullOrEmpty(Source); } protected override bool DoCheck() { Assert.ArgumentNotNullOrEmpty(Source, "Source"); foreach (string substring in Substrings) { if(DoesSourceContainSubstring(substring)) { return true; } } return false; } private bool DoesSourceContainSubstring(string substring) { if (IgnoreCase) { return !IsNotFoundIndex(Source.IndexOf(substring, StringComparison.CurrentCultureIgnoreCase)); } return !IsNotFoundIndex(Source.IndexOf(substring)); } private static bool IsNotFoundIndex(int index) { const int notFound = -1; return index == notFound; } public static ISubstringsChecker<string> CreateNewStringSubstringsContainer(IEnumerable<string> substrings) { return new StringSubstringsChecker(substrings); } public static ISubstringsChecker<string> CreateNewStringSubstringsContainer(IEnumerable<string> substrings, bool ignoreCase) { return new StringSubstringsChecker(substrings, ignoreCase); } public static ISubstringsChecker<string> CreateNewStringSubstringsContainer(string source, IEnumerable<string> substrings, bool ignoreCase) { return new StringSubstringsChecker(source, substrings, ignoreCase); } } }
In the version of our “checker” class above, I added the option to have the “checker” ignore case comparisons for the source and substrings.
Next, I created a new System.Web.Security.MembershipProvider subclass where I am utilizing the decorator pattern to decorate methods around changing passwords and creating users.
By default, we are instantiating an instance of the System.Web.Security.SqlMembershipProvider — this is what my local Sitecore sandbox instance is using, in order to get at the ASP.NET Membership tables in the core database.
using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Text; using System.Web.Security; using Sitecore.Configuration; using Sitecore.Diagnostics; using Sitecore.Security; using Sitecore.Sandbox.Utilities.StringUtilities; using Sitecore.Sandbox.Utilities.StringUtilities.Base; namespace Sitecore.Sandbox.Security.MembershipProviders { public class PreventDictionaryWordPasswordsMembershipProvider : MembershipProvider { private static IEnumerable<string> _DictionaryWords; private static IEnumerable<string> DictionaryWords { get { if (_DictionaryWords == null) { _DictionaryWords = GetDictionaryWords(); } return _DictionaryWords; } } public override string Name { get { return InnerMembershipProvider.Name; } } public override string ApplicationName { get { return InnerMembershipProvider.ApplicationName; } set { InnerMembershipProvider.ApplicationName = value; } } public override bool EnablePasswordReset { get { return InnerMembershipProvider.EnablePasswordReset; } } public override bool EnablePasswordRetrieval { get { return InnerMembershipProvider.EnablePasswordRetrieval; } } public override int MaxInvalidPasswordAttempts { get { return InnerMembershipProvider.MaxInvalidPasswordAttempts; } } public override int MinRequiredNonAlphanumericCharacters { get { return InnerMembershipProvider.MinRequiredNonAlphanumericCharacters; } } public override int MinRequiredPasswordLength { get { return InnerMembershipProvider.MinRequiredPasswordLength; } } public override int PasswordAttemptWindow { get { return InnerMembershipProvider.PasswordAttemptWindow; } } public override MembershipPasswordFormat PasswordFormat { get { return InnerMembershipProvider.PasswordFormat; } } public override string PasswordStrengthRegularExpression { get { return InnerMembershipProvider.PasswordStrengthRegularExpression; } } public override bool RequiresQuestionAndAnswer { get { return InnerMembershipProvider.RequiresQuestionAndAnswer; } } public override bool RequiresUniqueEmail { get { return InnerMembershipProvider.RequiresUniqueEmail; } } private MembershipProvider InnerMembershipProvider { get; set; } private ISubstringsChecker<string> DictionaryWordsSubstringsChecker { get; set; } public PreventDictionaryWordPasswordsMembershipProvider() : this(CreateNewSqlMembershipProvider(), CreateNewDictionaryWordsSubstringsChecker()) { } public PreventDictionaryWordPasswordsMembershipProvider(MembershipProvider innerMembershipProvider, ISubstringsChecker<string> dictionaryWordsSubstringsChecker) { SetInnerMembershipProvider(innerMembershipProvider); SetDictionaryWordsSubstringsChecker(dictionaryWordsSubstringsChecker); } private void SetInnerMembershipProvider(MembershipProvider innerMembershipProvider) { Assert.ArgumentNotNull(innerMembershipProvider, "innerMembershipProvider"); InnerMembershipProvider = innerMembershipProvider; } private void SetDictionaryWordsSubstringsChecker(ISubstringsChecker<string> dictionaryWordsSubstringsChecker) { Assert.ArgumentNotNull(dictionaryWordsSubstringsChecker, "dictionaryWordsSubstringsChecker"); DictionaryWordsSubstringsChecker = dictionaryWordsSubstringsChecker; } private static MembershipProvider CreateNewSqlMembershipProvider() { return new SqlMembershipProvider(); } private static ISubstringsChecker<string> CreateNewDictionaryWordsSubstringsChecker() { return CreateNewStringSubstringsChecker(DictionaryWords); } private static ISubstringsChecker<string> CreateNewStringSubstringsChecker(IEnumerable<string> substrings) { Assert.ArgumentNotNull(substrings, "substrings"); const bool ignoreCase = true; return StringSubstringsChecker.CreateNewStringSubstringsContainer(substrings, ignoreCase); } private static IEnumerable<string> GetDictionaryWords() { return Factory.GetStringSet("dictionaryWords/word"); } public override bool ChangePassword(string username, string oldPassword, string newPassword) { if (DoesPasswordContainDictionaryWord(newPassword)) { return false; } return InnerMembershipProvider.ChangePassword(username, oldPassword, newPassword); } private bool DoesPasswordContainDictionaryWord(string password) { Assert.ArgumentNotNullOrEmpty(password, "password"); DictionaryWordsSubstringsChecker.Source = password; return DictionaryWordsSubstringsChecker.ContainsSubstrings(); } public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer) { return InnerMembershipProvider.ChangePasswordQuestionAndAnswer(username, password, newPasswordQuestion, newPasswordAnswer); } public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { if (DoesPasswordContainDictionaryWord(password)) { status = MembershipCreateStatus.InvalidPassword; return null; } return InnerMembershipProvider.CreateUser(username, password, email, passwordQuestion, passwordAnswer, isApproved, providerUserKey, out status); } public override bool DeleteUser(string userName, bool deleteAllRelatedData) { return InnerMembershipProvider.DeleteUser(userName, deleteAllRelatedData); } public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { return InnerMembershipProvider.FindUsersByEmail(emailToMatch, pageIndex, pageSize, out totalRecords); } public override MembershipUserCollection FindUsersByName(string userNameToMatch, int pageIndex, int pageSize, out int totalRecords) { return InnerMembershipProvider.FindUsersByName(userNameToMatch, pageIndex, pageSize, out totalRecords); } public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords) { return InnerMembershipProvider.GetAllUsers(pageIndex, pageSize, out totalRecords); } public override int GetNumberOfUsersOnline() { return InnerMembershipProvider.GetNumberOfUsersOnline(); } public override string GetPassword(string username, string answer) { return InnerMembershipProvider.GetPassword(username, answer); } public override MembershipUser GetUser(object providerUserKey, bool userIsOnline) { return InnerMembershipProvider.GetUser(providerUserKey, userIsOnline); } public override MembershipUser GetUser(string username, bool userIsOnline) { return InnerMembershipProvider.GetUser(username, userIsOnline); } public override string GetUserNameByEmail(string email) { return InnerMembershipProvider.GetUserNameByEmail(email); } public override void Initialize(string name, NameValueCollection config) { InnerMembershipProvider.Initialize(name, config); } public override string ResetPassword(string username, string answer) { return InnerMembershipProvider.ResetPassword(username, answer); } public override bool UnlockUser(string userName) { return InnerMembershipProvider.UnlockUser(userName); } public override void UpdateUser(MembershipUser user) { InnerMembershipProvider.UpdateUser(user); } public override bool ValidateUser(string username, string password) { return InnerMembershipProvider.ValidateUser(username, password); } } }
In our MembershipProvider above, we have decorated the ChangePassword() and CreateUser() methods by employing a helper method to delegate to our DictionaryWordsSubstringsChecker instance — an instance of the StringSubstringsChecker class above — to see if the supplied password contains any of the fruits found in our collection, and prevent the workflow from moving forward in changing a user’s password, or creating a new user if one of the fruits is found in the provided password.
If we don’t find one of the fruits in the password, we then delegate to the inner MembershipProvider instance — this instance takes care of the rest around changing passwords, or creating new users.
I then had to register my MemberProvider in my Web.config — this cannot be placed in a patch include file since it lives outside of the <sitecore></sitecore> element.
<configuration> <membership defaultProvider="sitecore" hashAlgorithmType="SHA1"> <providers> <clear/> <add name="sitecore" type="Sitecore.Security.SitecoreMembershipProvider, Sitecore.Kernel" realProviderName="sql" providerWildcard="%" raiseEvents="true"/> <!-- our new provider --> <add name="sql" type="Sitecore.Sandbox.Security.MembershipProviders.PreventDictionaryWordPasswordsMembershipProvider, Sitecore.Sandbox" connectionStringName="core" applicationName="sitecore" minRequiredPasswordLength="6" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="256"/> <add name="switcher" type="Sitecore.Security.SwitchingMembershipProvider, Sitecore.Kernel" applicationName="sitecore" mappings="switchingProviders/membership"/> </providers> </membership> </configuration>
So, let’s see what the code above does.
I first tried to create a new user with a password containing a fruit.
I then used a fruit that was not in the collection of fruits above, and successfully created my new user.
Next, I tried to change an existing user’s password to one that contains the word “apple”.
I successfully changed this same user’s password using the word orange — it does not live in our collection of fruits above.
And that’s all there is to it. If you can think of alternative ways of doing this, or additional security features to implement, please drop a comment.
Until next time, have a Sitecoretastic day!