Encrypt Web Forms For Marketers Fields in Sitecore
In an earlier post, I walked you through how I experimented with data encryption of field values in Sitecore, and alluded to how I had done a similar thing for the Web Forms For Marketers (WFFM) module on a past project at work.
Months have gone by, and guilt has begun to gnaw away at my entire being — no, not really, I’m exaggerating a bit — but I definitely have been feeling bad for not sharing a solution.
In order to shake feeling bad, I decided to put my nose to the grindstone over the past few days to come up with a different solution than the one I had built at work, and this post shows the fruits of that labor.
I decided to reuse the interface I had created in my older post on data encryption. I am re-posting it here for reference:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Sitecore.Sandbox.Security.Encryption.Base { public interface IEncryptor { string Encrypt(string input); string Decrypt(string input); } }
I then asked myself “What encryption algorithm should I use?” I scavenged through the System.Security.Cryptography namespace in mscorlib.dll using .NET Reflector, and discovered some classes, when used together, achieve data encryption using the RC2 algorithm — an algorithm I know nothing about:
using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using Sitecore.Diagnostics; using Sitecore.Sandbox.Security.Encryption.Base; namespace Sitecore.Sandbox.Security.Encryption { public class RC2Encryptor : IEncryptor { public string Key { get; set; } private RC2Encryptor(string key) { SetKey(key); } private void SetKey(string key) { Assert.ArgumentNotNullOrEmpty(key, "key"); Key = key; } public string Encrypt(string input) { return Encrypt(input, Key); } public static string Encrypt(string input, string key) { byte[] inputArray = UTF8Encoding.UTF8.GetBytes(input); RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider(); rc2.Key = UTF8Encoding.UTF8.GetBytes(key); rc2.Mode = CipherMode.ECB; rc2.Padding = PaddingMode.PKCS7; ICryptoTransform cTransform = rc2.CreateEncryptor(); byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length); rc2.Clear(); return System.Convert.ToBase64String(resultArray, 0, resultArray.Length); } public string Decrypt(string input) { return Decrypt(input, Key); } public static string Decrypt(string input, string key) { byte[] inputArray = System.Convert.FromBase64String(input); RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider(); rc2.Key = UTF8Encoding.UTF8.GetBytes(key); rc2.Mode = CipherMode.ECB; rc2.Padding = PaddingMode.PKCS7; ICryptoTransform cTransform = rc2.CreateDecryptor(); byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length); rc2.Clear(); return UTF8Encoding.UTF8.GetString(resultArray); } public static IEncryptor CreateNewRC2Encryptor(string key) { return new RC2Encryptor(key); } } }
As I had mentioned in my previous post on data encryption, I am not a cryptography expert, nor a security expert.
I am not aware of how strong the RC2 encryption algorithm is, or what it would take to crack it. I strongly advise against using this algorithm in any production system without first consulting with a security expert. I am using it in this post only as an example.
If you happen to be a security expert, or are able to compare encryption algorithms defined in the System.Security.Cryptography namespace in mscorlib.dll, please share in a comment.
In a previous post on manipulating field values for WFFM forms, I had to define a new class that implements Sitecore.Forms.Data.IField in Sitecore.Forms.Core.dll in order to change field values — it appears the property mutator for the “out of the box” class is ignored — and decided to reuse it here:
using System; using Sitecore.Forms.Data; namespace Sitecore.Sandbox.Utilities.Manipulators.DTO { public class WFFMField : IField { public string Data { get; set; } public Guid FieldId { get; set; } public string FieldName { get; set; } public IForm Form { get; set; } public Guid Id { get; internal set; } public string Value { get; set; } } }
Next, I created a WFFM Data Provider that encrypts and decrypts field names and values:
using System; using System.Collections.Generic; using Sitecore.Configuration; using Sitecore.Diagnostics; using Sitecore.Forms.Data; using Sitecore.Forms.Data.DataProviders; using Sitecore.Reflection; using Sitecore.Sandbox.Security.DTO; using Sitecore.Sandbox.Security.Encryption.Base; using Sitecore.Sandbox.Security.Encryption; using Sitecore.Sandbox.Utilities.Manipulators.DTO; namespace Sitecore.Sandbox.WFFM.Forms.Data.DataProviders { public class WFFMEncryptionDataProvider : WFMDataProviderBase { private WFMDataProviderBase InnerProvider { get; set; } private IEncryptor Encryptor { get; set; } public WFFMEncryptionDataProvider(string innerProvider) : this(CreateInnerProvider(innerProvider), CreateDefaultEncryptor()) { } public WFFMEncryptionDataProvider(string connectionString, string innerProvider) : this(CreateInnerProvider(innerProvider, connectionString), CreateDefaultEncryptor()) { } public WFFMEncryptionDataProvider(WFMDataProviderBase innerProvider) : this(innerProvider, CreateDefaultEncryptor()) { } public WFFMEncryptionDataProvider(WFMDataProviderBase innerProvider, IEncryptor encryptor) { SetInnerProvider(innerProvider); SetEncryptor(encryptor); } private static WFMDataProviderBase CreateInnerProvider(string innerProvider, string connectionString = null) { Assert.ArgumentNotNullOrEmpty(innerProvider, "innerProvider"); if (!string.IsNullOrWhiteSpace(connectionString)) { return ReflectionUtil.CreateObject(innerProvider, new[] { connectionString }) as WFMDataProviderBase; } return ReflectionUtil.CreateObject(innerProvider, new object[0]) as WFMDataProviderBase; } private void SetInnerProvider(WFMDataProviderBase innerProvider) { Assert.ArgumentNotNull(innerProvider, "innerProvider"); InnerProvider = innerProvider; } private static IEncryptor CreateDefaultEncryptor() { return DataNullTerminatorEncryptor.CreateNewDataNullTerminatorEncryptor(GetEncryptorSettings()); } private static DataNullTerminatorEncryptorSettings GetEncryptorSettings() { return new DataNullTerminatorEncryptorSettings { EncryptionDataNullTerminator = Settings.GetSetting("WFFM.Encryption.DataNullTerminator"), InnerEncryptor = RC2Encryptor.CreateNewRC2Encryptor(Settings.GetSetting("WFFM.Encryption.Key")) }; } private void SetEncryptor(IEncryptor encryptor) { Assert.ArgumentNotNull(encryptor, "encryptor"); Encryptor = encryptor; } public override void ChangeStorage(Guid formItemId, string newStorage) { InnerProvider.ChangeStorage(formItemId, newStorage); } public override void ChangeStorageForForms(IEnumerable<Guid> ids, string storageName) { InnerProvider.ChangeStorageForForms(ids, storageName); } public override void DeleteForms(IEnumerable<Guid> formSubmitIds) { InnerProvider.DeleteForms(formSubmitIds); } public override void DeleteForms(Guid formItemId, string storageName) { InnerProvider.DeleteForms(formItemId, storageName); } public override IEnumerable<IPool> GetAbundantPools(Guid fieldId, int top, out int total) { return InnerProvider.GetAbundantPools(fieldId, top, out total); } public override IEnumerable<IForm> GetForms(QueryParams queryParams, out int total) { IEnumerable<IForm> forms = InnerProvider.GetForms(queryParams, out total); DecryptForms(forms); return forms; } public override IEnumerable<IForm> GetFormsByIds(IEnumerable<Guid> ids) { IEnumerable<IForm> forms = InnerProvider.GetFormsByIds(ids); DecryptForms(forms); return forms; } public override int GetFormsCount(Guid formItemId, string storageName, string filter) { return InnerProvider.GetFormsCount(formItemId, storageName, filter); } public override IEnumerable<IPool> GetPools(Guid fieldId) { return InnerProvider.GetPools(fieldId); } public override void InsertForm(IForm form) { EncryptForm(form); InnerProvider.InsertForm(form); } public override void ResetPool(Guid fieldId) { InnerProvider.ResetPool(fieldId); } public override IForm SelectSingleForm(Guid fieldId, string likeValue) { IForm form = InnerProvider.SelectSingleForm(fieldId, likeValue); DecryptForm(form); return form; } public override bool UpdateForm(IForm form) { EncryptForm(form); return InnerProvider.UpdateForm(form); } private void EncryptForms(IEnumerable<IForm> forms) { Assert.ArgumentNotNull(forms, "forms"); foreach (IForm form in forms) { EncryptForm(form); } } private void EncryptForm(IForm form) { Assert.ArgumentNotNull(form, "form"); Assert.ArgumentNotNull(form.Field, "form.Field"); form.Field = EncryptFields(form.Field); } private IEnumerable<IField> EncryptFields(IEnumerable<IField> fields) { Assert.ArgumentNotNull(fields, "fields"); IList<IField> encryptedFields = new List<IField>(); foreach (IField field in fields) { encryptedFields.Add(EncryptField(field)); } return encryptedFields; } private IField EncryptField(IField field) { Assert.ArgumentNotNull(field, "field"); return CreateNewWFFMField(field, Encrypt(field.FieldName), Encrypt(field.Value)); } private void DecryptForms(IEnumerable<IForm> forms) { Assert.ArgumentNotNull(forms, "forms"); foreach (IForm form in forms) { DecryptForm(form); } } private void DecryptForm(IForm form) { Assert.ArgumentNotNull(form, "form"); Assert.ArgumentNotNull(form.Field, "form.Field"); form.Field = DecryptFields(form.Field); } private IEnumerable<IField> DecryptFields(IEnumerable<IField> fields) { Assert.ArgumentNotNull(fields, "fields"); IList<IField> decryptedFields = new List<IField>(); foreach (IField field in fields) { decryptedFields.Add(DecryptField(field)); } return decryptedFields; } private IField DecryptField(IField field) { Assert.ArgumentNotNull(field, "field"); return CreateNewWFFMField(field, Decrypt(field.FieldName), Decrypt(field.Value)); } private string Encrypt(string input) { return Encryptor.Encrypt(input); } private string Decrypt(string input) { return Encryptor.Decrypt(input); } private static IField CreateNewWFFMField(IField field, string fieldName, string value) { if (field != null) { return new WFFMField { Data = field.Data, FieldId = field.FieldId, FieldName = fieldName, Form = field.Form, Id = field.Id, Value = value }; } return null; } } }
The above class employs the decorator pattern. An inner WFFM Data Provider — which is supplied via a parameter configuration node in \App_Config\Include\forms.config, and is created via magic within the Sitecore.Reflection.ReflectionUtil class — is wrapped.
Methods that save and retrieve form data in the above Data Provider decorate the same methods defined on the inner WFFM Data Provider.
Methods that save form data pass form(s) — and eventually their fields — through a chain of Encrypt methods. The Encrypt method that takes in an IField instance as an argument encrypts the instance’s field name and value, and returns a new instance of the WFFMField class using the encrypted data and the other properties on the IField instance untouched.
Similarly, a chain of Decrypt methods are called for form(s) being retrieved from the inner Data Provider — field names and values are decrypted and saved into a new instance of the WFFMField class, and the manipulated form(s) are returned.
I want to point out that the IEncryptor instance is actually an instance of DataNullTerminatorEncryptor — see my earlier post on data encryption to see how this is implemented — which decorates our RC2Encryptor. This decorating encryptor stamps encrypted strings with a special string so we don’t accidentally encrypt a value twice, and it also won’t try to decrypt a string value that isn’t encrypted.
I added a new include configuration file to hold encryption related settings — the IEncryptor’s key, and the string that will be put at the end of all encrypted data via the DataNullTerminatorEncryptor instance:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <settings> <!-- TODO: change the terminator so it does not scream "PLEASE TRY TO CRACK ME!" --> <setting name="WFFM.Encryption.DataNullTerminator" value="#I_AM_ENCRYPTED#" /> <!-- I found this key somewhere on the internet, so it must be secure --> <setting name="WFFM.Encryption.Key" value="88bca90e90875a" /> </settings> </sitecore> </configuration>
I then hooked in the encryption WFFM Data Provider in \App_Config\Include\forms.config, and set the type for the inner provider:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/"> <sitecore> <!-- There is stuff up here --> <!-- MS SQL --> <formsDataProvider type="Sitecore.Sandbox.WFFM.Forms.Data.DataProviders.WFFMEncryptionDataProvider, Sitecore.Sandbox"> <!-- No, this is not my real connection string --> <param desc="connection string">user id=(user);password=(password);Data Source=(database)</param> <param desc="inner provider">Sitecore.Forms.Data.DataProviders.WFMDataProvider, Sitecore.Forms.Core</param> </formsDataProvider> <!-- There is stuff down here --> </sitecore> </configuration>
Let’s see this in action.
I created a new WFFM form with some fields for testing:
I then mapped the above form to a new page in Sitecore, and published both the form and page.
I navigated to the form page, and filled it out:
After clicking submit, I was given a ‘thank you’ page’:
Let’s see what our field data looks like in the WFFM database:
As you can see, the data is encrypted.
Now, let’s see if the data is also encrypted in the Forms Report for our test form:
As you can see, the end-user would be unaware that any data manipulation is happening under the hood.
If you have any thoughts on this, please leave a comment.
Publish Items With the Sitecore Item Web API Using a Custom ResolveAction itemWebApiRequest Pipeline Processor
At the end of last week, when many people were probably thinking about what to do over the weekend, or were making plans with family and/or friends, I started thinking about what I might need to do next on the project I’m working on.
I realized I might need a way to publish Sitecore items I’ve touched via the Sitecore Item Web API — a feature that appears to be missing, or I just cannot figure out how to use from its documentation (if there is a way to do this “out of the box”, please share in a comment below).
After some digging around in Sitecore.ItemWebApi.dll and \App_Config\Include\Sitecore.ItemWebApi.config, I thought it would be a good idea to define a new action that employs a request method other than get, post, put, delete — these request methods are used by a vanilla install of the Sitecore Item Web API.
Where would one find a list of “standard” request methods? Of course Google knows all — I learned about the patch request method, and decided to use it.
According to Wikipedia — see this entry subsection discussing request methods — the patch request method is “used to apply partial modifications to a resource”, and one could argue that a publishing an item in Sitecore is a partial modification to that item — it’s being pushed to another database which is really an update on that item in the target database.
Now that our research is behind us — and we’ve learned about the patch request method — let’s get our hands dirty with some code.
Following the pipeline processor convention set forth in code for the Sitecore Item Web API for other request methods, I decide to box our new patch requests into a new pipeline, and doing this called for creating a new data transfer object for the new pipeline processor we will define below:
using Sitecore.Data.Items; using Sitecore.ItemWebApi.Pipelines; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Patch.DTO { public class PatchArgs : OperationArgs { public PatchArgs(Item[] scope) : base(scope) { } } }
Next, I created a base class for our new patch processor:
using Sitecore.ItemWebApi.Pipelines; using Sitecore.Sandbox.ItemWebApi.Pipelines.Patch.DTO; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Patch { public abstract class PatchProcessor : OperationProcessor<PatchArgs> { protected PatchProcessor() { } } }
I then created a new pipeline processor that will publish items passed to it:
using System; using System.Collections.Generic; using System.Linq; using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.ItemWebApi; using Sitecore.Publishing; using Sitecore.Web; using Sitecore.Sandbox.ItemWebApi.Pipelines.Patch.DTO; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Patch { public class PublishScope : PatchProcessor { private string DefaultTargetDatabase { get; set; } public override void Process(PatchArgs arguments) { Assert.ArgumentNotNull(arguments, "arguments"); Assert.IsNotNull(arguments.Scope, "The scope is null!"); PublishItems(arguments.Scope, GetTargetDatabase()); arguments.Result = GetResult(arguments.Scope); } private Database GetTargetDatabase() { return Factory.GetDatabase(GetTargetDatabaseName()); } private string GetTargetDatabaseName() { string databaseName = WebUtil.GetQueryString("sc_target_database"); if(!string.IsNullOrWhiteSpace(databaseName)) { return databaseName; } Assert.IsNotNullOrEmpty(DefaultTargetDatabase, "DefaultTargetDatabase must be set!"); return DefaultTargetDatabase; } private static void PublishItems(IEnumerable<Item> items, Database database) { foreach(Item item in items) { PublishItem(item, database); } } private static void PublishItem(Item item, Database database) { PublishOptions publishOptions = new PublishOptions ( item.Database, database, Sitecore.Publishing.PublishMode.SingleItem, item.Language, DateTime.Now ); Publisher publisher = new Publisher(publishOptions); publisher.Options.RootItem = item; publisher.Publish(); } private static Dynamic GetResult(IEnumerable<Item> scope) { Assert.ArgumentNotNull(scope, "scope"); Dynamic dynamic = new Dynamic(); dynamic["statusCode"] = 200; dynamic["result"] = GetInnerResult(scope); return dynamic; } private static Dynamic GetInnerResult(IEnumerable<Item> scope) { Assert.ArgumentNotNull(scope, "scope"); Dynamic dynamic = new Dynamic(); dynamic["count"] = scope.Count(); dynamic["itemIds"] = scope.Select(item => item.ID.ToString()); return dynamic; } } }
The above pipeline processor class serially publishes each item passed to it, and returns a Sitecore.ItemWebApi.Dynamic instance containing information on how many items were published; a collection of IDs of the items that were published; and an OK status code.
If the calling code does not supply a publishing target database via the sc_target_database query string parameter, the processor will use the value defined in DefaultTargetDatabase — this value is set in \App_Config\Include\Sitecore.ItemWebApi.config, which you will see later when I show changes I made to this file.
It had been awhile since I’ve had to publish items in code, so I searched for a refresher on how to do this.
In my quest for some Sitecore API code, I rediscovered this article by Sitecore MVP
Brian Pedersen showing how one can publish Sitecore items programmatically — many thanks to Brian for this article!
If you haven’t read Brian’s article, you should go check it out now. Don’t worry, I’ll wait. 🙂
I then created a new ResolveAction itemWebApiRequest pipeline processor:
using System; using Sitecore.Diagnostics; using Sitecore.Exceptions; using Sitecore.ItemWebApi.Pipelines.Request; using Sitecore.ItemWebApi.Security; using Sitecore.Pipelines; using Sitecore.Sandbox.ItemWebApi.Pipelines.Patch.DTO; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request { public class ResolveAction : Sitecore.ItemWebApi.Pipelines.Request.ResolveAction { public override void Process(RequestArgs requestArgs) { Assert.ArgumentNotNull(requestArgs, "requestArgs"); string method = GetMethod(requestArgs.Context); AssertOperation(requestArgs, method); if(IsCreateRequest(method)) { ExecuteCreateRequest(requestArgs); return; } if(IsReadRequest(method)) { ExecuteReadRequest(requestArgs); return; } if(IsUpdateRequest(method)) { ExecuteUpdateRequest(requestArgs); return; } if(IsDeleteRequest(method)) { ExecuteDeleteRequest(requestArgs); return; } if (IsPatchRequest(method)) { ExecutePatchRequest(requestArgs); return; } } private static void AssertOperation(RequestArgs requestArgs, string method) { Assert.ArgumentNotNull(requestArgs, "requestArgs"); if (requestArgs.Context.Settings.Access == AccessType.ReadOnly && !AreEqual(method, "get")) { throw new AccessDeniedException("The operation is not allowed."); } } private static bool IsCreateRequest(string method) { return AreEqual(method, "post"); } private static bool IsReadRequest(string method) { return AreEqual(method, "get"); } private static bool IsUpdateRequest(string method) { return AreEqual(method, "put"); } private static bool IsDeleteRequest(string method) { return AreEqual(method, "delete"); } private static bool IsPatchRequest(string method) { return AreEqual(method, "patch"); } private static bool AreEqual(string one, string two) { return string.Equals(one, two, StringComparison.InvariantCultureIgnoreCase); } protected virtual void ExecutePatchRequest(RequestArgs requestArgs) { Assert.ArgumentNotNull(requestArgs, "requestArgs"); PatchArgs patchArgsArgs = new PatchArgs(requestArgs.Scope); CorePipeline.Run("itemWebApiPatch", patchArgsArgs); requestArgs.Result = patchArgsArgs.Result; } private string GetMethod(Sitecore.ItemWebApi.Context context) { Assert.ArgumentNotNull(context, "context"); return context.HttpContext.Request.HttpMethod.ToLower(); } } }
The class above contains the same logic as Sitecore.ItemWebApi.Pipelines.Request.ResolveAction, though I refactored it a bit — the nested conditional statements in the Process method were driving me bonkers, and I atomized logic by placing into new methods.
Plus, I added an additional check to see if the request we are to execute is a patch request — this is true when HttpContext.Request.HttpMethod.ToLower() in our Sitecore.ItemWebApi.Context instance is equal to “patch” — and call our new patch pipeline if this is the case.
I then added the new itemWebApiPatch pipeline with its new PublishScope processor, and replaced /configuration/sitecore/pipelines/itemWebApiRequest/processor[@type=”Sitecore.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.ItemWebApi”] with /configuration/sitecore/pipelines/itemWebApiRequest/processor[@type=”Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.Sandbox”] in \App_Config\Include\Sitecore.ItemWebApi.config:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <itemWebApiRequest> <!-- stuff is defined up here --> <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.Sandbox" /> <!-- stuff is defined right here --> </itemWebApiRequest> <!-- more stuff is defined here --> <itemWebApiPatch> <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Patch.PublishScope, Sitecore.Sandbox"> <DefaultTargetDatabase>web</DefaultTargetDatabase> </processor> </itemWebApiPatch> </pipelines> <!-- there's even more stuff defined down here --> </sitecore> </configuration>
Let’s test this out, and see how we did.
We’ll be publishing these items:
As you can see, they aren’t in the web database right now:
I had to modify code in my copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK, to use the patch request method for the ancestor home item shown above, and set a scope of self and recursive (scope=s|r) — checkout out this post where I added the recursive axe to the Sitecore Item Web API. I am excluding the console application code modification for the sake of brevity.
I then ran the console application above, and saw this:
All of these items now live in the web database:
If you have any thoughts on this, or ideas on other useful actions for the Sitecore Item Web API, please drop a comment.
Expand Your Scope: Add Additional Axes Via a Custom Sitecore Item Web API itemWebApiRequest Pipeline Processor
The Sitecore Item Web API offers client code the choice of retrieving an Item’s parent, the Item itself, all of its children, or any combination of these by simply setting the scope query string parameter in the request.
For example, if you want an Item’s children, you would only set the scope query string parameter to be the axe “c” — this would be scope=c — or if you wanted all data for the Item and its children, you would just set the scope query string parameter to be the self and children axes separated by a pipe — e.g. scope=s|c. Multiple axes must be separated by a pipe.
The other day, however, for my current project, I realized I needed a way to retrieve all data for an Item and all of its descendants via the Sitecore Item Web API.
The three options that ship with the Sitecore Item Web API cannot help me here, unless I want to make multiple requests to get data for an Item and all of it’s children, and then loop over all children and get their children, ad infinitum (well, hopefully it does stop somewhere).
Such a solution would require more development time — I would have to write additional code to do all of the looping — and this would — without a doubt — yield poorer performance versus getting all data upfront in a single request.
Through my excavating efforts in \App_Config\Include\Sitecore.ItemWebApi.config and Sitecore.ItemWebApi.dll, I discovered we can replace this “out of the box” functionality — this lives in /configuration/sitecore/pipelines/itemWebApiRequest/processor[@type=”Sitecore.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.ItemWebApi”] in the Sitecore.ItemWebApi.config — via a custom itemWebApiRequest pipeline processor.
I thought it would be a good idea to define each of our scope operations in its own pipeline processor, albeit have all of these pipeline processors be nested within our itemWebApiRequest pipeline processor.
For the lack of a better term, I’m calling each of these a scope sub-pipeline processor (if you can think of a better name, or have seen this approach done before, please drop a comment).
The first thing I did was create a custom processor class to house two additional properties for our sub-pipeline processor:
using System.Xml; using Sitecore.Pipelines; using Sitecore.Diagnostics; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request { public class ScopeProcessor : Processor { public string Suppresses { get; private set; } public string QueryString { get; private set; } public ScopeProcessor(XmlNode configNode) : base(GetAttributeValue(configNode, "name"), GetAttributeValue(configNode, "type"), GetAttributeValue(configNode, "methodName")) { Suppresses = GetAttributeValue(configNode, "suppresses"); QueryString = GetAttributeValue(configNode, "queryString"); } public ScopeProcessor(string name, string type, string methodName, string suppresses, string queryString) : base(name, type, methodName) { Suppresses = suppresses; QueryString = queryString; } private static string GetAttributeValue(XmlNode configNode, string attributeName) { Assert.ArgumentNotNull(configNode, "configNode"); Assert.ArgumentNotNullOrEmpty(attributeName, "attributeName"); XmlAttribute attribute = configNode.Attributes[attributeName]; if (attribute != null) { return attribute.Value; } return string.Empty; } } }
The QueryString property will contain the axe for the given scope, and Suppresses property maps to another scope sub-pipeline processor query string value that will be ignored when both are present.
I then created a new PipelineArgs class for the scope sub-pipeline processors:
using System.Collections.Generic; using Sitecore.Data.Items; using Sitecore.Pipelines; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request.DTO { public class ScopeProcessorRequestArgs : PipelineArgs { private List<Item> _Items; public List<Item> Items { get { if (_Items == null) { _Items = new List<Item>(); } return _Items; } set { _Items = value; } } private List<Item> _Scope; public List<Item> Scope { get { if (_Scope == null) { _Scope = new List<Item>(); } return _Scope; } set { _Scope = value; } } public ScopeProcessorRequestArgs() { } } }
Basically, the above class just holds Items that will be processed, and keeps track of Items in scope — these Items are added via the scope sub-pipeline processors for the supplied axes.
Now it’s time for our itemWebApiRequest pipeline processor:
using System.Collections.Generic; using System.Linq; using System.Xml; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.ItemWebApi; using Sitecore.ItemWebApi.Pipelines.Request; using Sitecore.Web; using Sitecore.Sandbox.ItemWebApi.Pipelines.Request.DTO; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request { public class ResolveScope : RequestProcessor { private IDictionary<string, ScopeProcessor> _ScopeProcessors; private IDictionary<string, ScopeProcessor> ScopeProcessors { get { if(_ScopeProcessors == null) { _ScopeProcessors = new Dictionary<string, ScopeProcessor>(); } return _ScopeProcessors; } } public override void Process(RequestArgs arguments) { if(!HasItemsInSet(arguments)) { return; } arguments.Scope = GetItemsInScope(arguments).ToArray(); } private static bool HasItemsInSet(RequestArgs arguments) { Assert.ArgumentNotNull(arguments, "arguments"); Assert.ArgumentNotNull(arguments.Items, "arguments.Items"); if (arguments.Items.Length < 1) { Logger.Warn("Cannot resolve the scope because the item set is empty."); arguments.Scope = new Item[0]; return false; } return true; } private IEnumerable<Item> GetItemsInScope(RequestArgs arguments) { List<Item> itemsInScope = new List<Item>(); foreach (Item item in arguments.Items) { ScopeProcessorRequestArgs args = new ScopeProcessorRequestArgs(); args.Items.Add(item); GetItemsInScope(args); itemsInScope.AddRange(args.Scope); } return itemsInScope; } private void GetItemsInScope(ScopeProcessorRequestArgs arguments) { IEnumerable<ScopeProcessor> scopeProcessors = GetScopeProcessorsForRequest(); foreach (ScopeProcessor scopeProcessor in scopeProcessors) { scopeProcessor.Invoke(arguments); } } private IEnumerable<ScopeProcessor> GetScopeProcessorsForRequest() { List<ScopeProcessor> scopeProcessors = GetScopeProcessorsForAxes(); List<ScopeProcessor> scopeProcessorsForRequest = new List<ScopeProcessor>(); foreach(ScopeProcessor scopeProcessor in scopeProcessors) { bool canAddProcessor = !scopeProcessors.Exists(processor => processor.Suppresses.Equals(scopeProcessor.QueryString)); if (canAddProcessor) { scopeProcessorsForRequest.Add(scopeProcessor); } } return scopeProcessorsForRequest; } private List<ScopeProcessor> GetScopeProcessorsForAxes() { List<ScopeProcessor> scopeProcessors = new List<ScopeProcessor>(); foreach (string axe in GetAxes()) { ScopeProcessor scopeProcessor; ScopeProcessors.TryGetValue(axe, out scopeProcessor); if(scopeProcessor != null && !scopeProcessors.Contains(scopeProcessor)) { scopeProcessors.Add(scopeProcessor); } } return scopeProcessors; } private IEnumerable<string> GetAxes() { string queryString = WebUtil.GetQueryString("scope", null); if (string.IsNullOrWhiteSpace(queryString)) { return new string[] { "s" }; } return queryString.Split(new char[] { '|' }).Distinct(); } private IEnumerable<string> GetScopeProcessorQueryStringValues() { return ScopeProcessors.Values.Select(scopeProcessors => scopeProcessors.QueryString).ToList(); } public virtual void AddScopeProcessor(XmlNode configNode) { ScopeProcessor scopeProcessor = new ScopeProcessor(configNode); bool canAdd = !string.IsNullOrEmpty(scopeProcessor.QueryString) && !ScopeProcessors.ContainsKey(scopeProcessor.QueryString); if (canAdd) { ScopeProcessors.Add(scopeProcessor.QueryString, scopeProcessor); } } public virtual void AddItemSelf(ScopeProcessorRequestArgs arguments) { foreach (Item item in arguments.Items) { arguments.Scope.AddRange(GetCanBeReadItems(new Item[] { item })); } } public virtual void AddItemParent(ScopeProcessorRequestArgs arguments) { foreach (Item item in arguments.Items) { arguments.Scope.AddRange(GetCanBeReadItems(new Item[] { item.Parent })); } } public virtual void AddItemDescendants(ScopeProcessorRequestArgs arguments) { foreach (Item item in arguments.Items) { arguments.Scope.AddRange(GetCanBeReadItems(item.Axes.GetDescendants())); } } public virtual void AddItemChildren(ScopeProcessorRequestArgs arguments) { foreach(Item item in arguments.Items) { arguments.Scope.AddRange(GetCanBeReadItems(item.GetChildren())); } } private static IEnumerable<Item> GetCanBeReadItems(IEnumerable<Item> list) { if (list == null) { return new List<Item>(); } return list.Where(item => CanReadItem(item)); } private static bool CanReadItem(Item item) { return Context.Site.Name != "shell" && item.Access.CanRead() && item.Access.CanReadLanguage(); } } }
When this class is instantiated, each scope sub-pipeline processor is added to a dictionary, keyed by its query string axe value.
When this processor is invoked, it performs some validation — similarly to what is being done in the “out of the box” Sitecore.ItemWebApi.Pipelines.Request.ResolveScope class — and determines which scope processors are applicable for the given request. Only those that found in the dictionary via the supplied axes are used, minus those that are suppressed.
Once the collection of scope sub-pipeline processors is in place, each are invoked with a ScopeProcessorRequestArgs instance containing an Item to be processed.
When a scope sub-pipeline processor is done executing, Items that were retrieved from it are added into master list of scope Items to be returned to the caller.
I then glued all of this together — including the scope sub-pipeline processors — in \App_Config\Include\Sitecore.ItemWebApi.config:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <!-- stuff is defined up here too --> <itemWebApiRequest> <!-- stuff is defined up here --> <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox"> <scopeProcessors hint="raw:AddScopeProcessor"> <scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemSelf" name="self" queryString="s" /> <scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemParent" name="parent" queryString="p" /> <scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemDescendants" name="recursive" queryString="r" suppresses="c" /> <scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemChildren" name="children" queryString="c" /> </scopeProcessors> </processor> <!-- some stuff is defined down here --> </itemWebApiRequest> </pipelines> <!-- there's more stuff defined down here --> </sitecore> </configuration>
Let’s take the above for a spin.
First we need some items for testing. Lucky for me, I hadn’t cleaned up after myself when creating a previous blog post — yes, now I have a legitimate excuse for not picking up after myself — so let’s use these for testing:
After modifying some code in my copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK — I updated which item we are requesting conjoined for our scope query string parameter (scope=r) — I launched it to retrieve our test items:
If you have any thoughts on this, or ideas on improving the above, please leave a comment.
Enforce Password Expiration in the Sitecore CMS
I recently worked on a project that called for a feature to expire Sitecore users’ passwords after an elapsed period of time since their passwords were last changed.
The idea behind this is to lessen the probability that an attacker will infiltrate a system — or multiple systems if users reuse their passwords across different applications (this is more common than you think) — within an organization by acquiring old database backups containing users’ passwords.
Since I can’t show you what I built for that project, I cooked up another solution — a custom loggingin processor that determines whether a user’s password has expired in Sitecore:
using System; using System.Web.Security; using Sitecore.Diagnostics; using Sitecore.Pipelines.LoggingIn; using Sitecore.Web; namespace Sitecore.Sandbox.Pipelines.LoggingIn { public class CheckPasswordExpiration { private TimeSpan TimeSpanToExpirePassword { get; set; } private string ChangePasswordPageUrl { get; set; } public void Process(LoggingInArgs args) { Assert.ArgumentNotNull(args, "args"); if (!IsEnabled()) { return; } MembershipUser user = GetMembershipUser(args); if (HasPasswordExpired(user)) { WebUtil.Redirect(ChangePasswordPageUrl); } } private bool IsEnabled() { return IsTimeSpanToExpirePasswordSet() && IsChangePasswordPageUrlSet(); } private bool IsTimeSpanToExpirePasswordSet() { return TimeSpanToExpirePassword > default(TimeSpan); } private bool IsChangePasswordPageUrlSet() { return !string.IsNullOrWhiteSpace(ChangePasswordPageUrl); } private static MembershipUser GetMembershipUser(LoggingInArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNullOrEmpty(args.Username, "args.Username"); return Membership.GetUser(args.Username, false); } private bool HasPasswordExpired(MembershipUser user) { return user.LastPasswordChangedDate.Add(TimeSpanToExpirePassword) <= DateTime.Now; } } }
The processor above ascertains whether a user’s password has expired by adding a configured timespan — see the configuration file below — to the last date and time the password was changed, and if that date and time summation is in the past — this means the password should have been changed already — then we redirect the user to a change password page (this is configured to be the Change Password page in Sitecore).
I wired up the custom loggingin processor, its timespan on expiring passwords — here I am using 1 minute since I can’t wait around all day 😉 — and set the change password page to be the url of Sitecore’s Change Password page:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <processors> <loggingin> <processor mode="on" type="Sitecore.Sandbox.Pipelines.LoggingIn.CheckPasswordExpiration, Sitecore.Sandbox" patch:before="processor[@type='Sitecore.Pipelines.LoggingIn.CheckStartPage, Sitecore.Kernel']"> <!-- Number of days, hours, minutes and seconds after the last password change date to expire passwords --> <TimeSpanToExpirePassword>00:00:01:00</TimeSpanToExpirePassword> <ChangePasswordPageUrl>/sitecore/login/changepassword.aspx</ChangePasswordPageUrl> </processor> </loggingin> </processors> </sitecore> </configuration>
Let’s test this out.
I went to Sitecore’s login page, and entered my username and password on the login form:
I clicked the Login button, and was redirected to the Change Password page as expected:
If you can think of any other security measures that should be added to Sitecore, please share in a comment.
Go Green: Put Items in the Recycle Bin When Deleting Via the Sitecore Item Web API
This morning I discovered that items are permanently deleted by the Sitecore Item Web API during a delete action. This is probably called out somewhere in its developer’s guide but I don’t recall having read this.
Regardless of whether it’s highlighted somewhere in documentation, I decided to investigate why this happens.
After combing through Sitecore Item Web API pipelines defined in \App_Config\Include\Sitecore.ItemWebApi.config and code in Sitecore.ItemWebApi.dll, I honed in on the following:
This above code lives in the only itemWebApiDelete pipeline processor that comes with the Sitecore Item Web API, and this processor can be found at /configuration/sitecore/pipelines/itemWebApiDelete/processor[@type=”Sitecore.ItemWebApi.Pipelines.Delete.DeleteScope, Sitecore.ItemWebApi”] in the \App_Config\Include\Sitecore.ItemWebApi.config file.
I don’t know about you, but I’m not always comfortable with deleting items permanently in Sitecore. I heavily rely on Sitecore’s Recycle Bin — yes, I have deleted items erroneously in the past, but recovered quickly by restoring them from the Recycle Bin (I hope I’m not the only one who has done this. :-/)
Unearthing the above prompted me to write a new itemWebApiDelete pipeline processor that puts items in the Recycle Bin when the Recycle Bin setting — see /configuration/sitecore/settings/setting[@name=”RecycleBinActive”] in the Web.config — is enabled:
using System.Collections.Generic; using System.Linq; using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.ItemWebApi; using Sitecore.ItemWebApi.Pipelines.Delete; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Delete { public class RecycleScope : DeleteProcessor { private const int OKStatusCode = 200; public override void Process(DeleteArgs arguments) { Assert.ArgumentNotNull(arguments, "arguments"); IEnumerable<Item> itemsToDelete = arguments.Scope; DeleteItems(itemsToDelete); arguments.Result = GetStatusInformation(OKStatusCode, GetDeletionInformation(itemsToDelete)); } private static void DeleteItems(IEnumerable<Item> itemsToDelete) { foreach (Item itemToDelete in itemsToDelete) { DeleteItem(itemToDelete); } } private static void DeleteItem(Item itemToDelete) { Assert.ArgumentNotNull(itemToDelete, "itemToDelete"); // put items in the recycle bin if it's turned on if (Settings.RecycleBinActive) { itemToDelete.Recycle(); } else { itemToDelete.Delete(); } } private static Dynamic GetDeletionInformation(IEnumerable<Item> itemsToDelete) { return GetDeletionInformation(itemsToDelete.Count(), GetItemIds(itemsToDelete)); } private static Dynamic GetDeletionInformation(int count, IEnumerable<ID> itemIds) { Dynamic deletionInformation = new Dynamic(); deletionInformation["count"] = count; deletionInformation["itemIds"] = itemIds.Select(id => id.ToString()); return deletionInformation; } private static IEnumerable<ID> GetItemIds(IEnumerable<Item> items) { Assert.ArgumentNotNull(items, "items"); return items.Select(item => item.ID); } private static Dynamic GetStatusInformation(int statusCode, Dynamic result) { Assert.ArgumentNotNull(result, "result"); Dynamic status = new Dynamic(); status["statusCode"] = statusCode; status["result"] = result; return status; } } }
There really isn’t anything magical about the code above. It utilizes most of the same logic that comes with the itemWebApiDelete pipeline processor that ships with the Sitecore Item Web API, although I did move code around into new methods.
The only major difference is the invocation of the Recycle method on item instances when the Recycle Bin is enabled in Sitecore. If the Recycle Bin is not enabled, we call the Delete method instead — as does the “out of the box” pipeline processor.
I then replaced the existing itemWebApiDelete pipeline processor in \App_Config\Include\Sitecore.ItemWebApi.config with our new one defined above:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <!-- stuff is defined up here --> <itemWebApiDelete> <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Delete.RecycleScope, Sitecore.Sandbox" /> </itemWebApiDelete> <!-- there's more stuff defined down here --> </sitecore> </configuration>
Let’s see this in action.
We first need a test item. Let’s create one together:
I then tweaked the delete method in my copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK, to point to our test item in the master database — I have omitted this code for the sake of brevity — and then ran the console application calling the delete method only:
As you can see, our test item is now in the Recycle Bin:
If you have any thoughts on this, please leave a comment.
Add Additional Item Properties in Sitecore Item Web API Responses
The other day I was exploring pipelines of the Sitecore Item Web API, and took note of the itemWebApiGetProperties pipeline. This pipeline adds information about an item in the response returned by the Sitecore Item Web API. You can find this pipeline at /configuration/sitecore/pipelines/itemWebApiGetProperties in \App_Config\Include\Sitecore.ItemWebApi.config.
The following properties are set for an item in the response via the lonely pipeline processor — /configuration/sitecore/pipelines/itemWebApiGetProperties/processor[@type=”Sitecore.ItemWebApi.Pipelines.GetProperties.GetProperties, Sitecore.ItemWebApi”] — that ships with the Sitecore Item Web API:
Here’s an example of what the properties set by the above pipeline processor look like in the response — I invoked a read request to the Sitecore Item Web API via a copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK:
You might be asking “how difficult would it be to add in my own properties?” It’s not difficult at all!
I whipped up the following itemWebApiGetProperties pipeline processor to show how one can add more properties for an item:
using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.ItemWebApi; using Sitecore.ItemWebApi.Pipelines.GetProperties; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.GetProperties { public class GetEvenMoreProperties : GetPropertiesProcessor { public override void Process(GetPropertiesArgs arguments) { Assert.ArgumentNotNull(arguments, "arguments"); arguments.Properties.Add("ParentID", arguments.Item.ParentID.ToString()); arguments.Properties.Add("ChildrenCount", arguments.Item.Children.Count); arguments.Properties.Add("Level", arguments.Item.Axes.Level); arguments.Properties.Add("IsItemClone", arguments.Item.IsItemClone); arguments.Properties.Add("CreatedBy", arguments.Item["__Created by"]); arguments.Properties.Add("UpdatedBy", GetItemUpdatedBy(arguments.Item)); } private static Dynamic GetItemUpdatedBy(Item item) { Assert.ArgumentNotNull(item, "item"); string[] usernamePieces = item["__Updated by"].Split('\\'); Dynamic username = new Dynamic(); if (usernamePieces.Length > 1) { username["Domain"] = usernamePieces[0]; username["Username"] = usernamePieces[1]; } else if (usernamePieces.Length > 0) { username["Username"] = usernamePieces[0]; } return username; } } }
The ParentID, ChildrenCount, Level and IsItemClone properties are simply added to the properties SortedDictionary within the GetPropertiesArgs instance, and will be serialized as is.
For the UpdatedBy property, I decided to leverage the Sitecore.ItemWebApi.Dynamic class in order to have the username set in the “__Updated by” field be represented by a JSON object. This JSON object sets the domain and username — without the domain — into different JSON properties.
As a side note — when writing your own service code for the Sitecore Item Web API — I strongly recommend using instances of the Sitecore.ItemWebApi.Dynamic class — or something similar — for complex objects. Developers writing code to consume your JSON will thank you many times for it. 🙂
I registered my new processor to the itemWebApiGetProperties pipeline in my Sitecore instance’s \App_Config\Include\Sitecore.ItemWebApi.config:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <!-- there's stuff here --> <itemWebApiGetProperties> <processor type="Sitecore.ItemWebApi.Pipelines.GetProperties.GetProperties, Sitecore.ItemWebApi" /> <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.GetProperties.GetEvenMoreProperties, Sitecore.Sandbox" /> </itemWebApiGetProperties> <!-- there's stuff here as well --> </sitecore> </configuration>
Let’s take this for a spin.
I ran the console application again to see what the response now looks like:
As you can see, our additional properties are now included in the response.
If you can think of other item properties that would be useful for Sitecore Item Web API client applications, please share in a comment.
Until next time, have a Sitecorelicious day!
Tailor Sitecore Item Web API Field Values On Read
Last week Sitecore MVP Kamruz Jaman asked me in this tweet if I could answer this question on Stack Overflow.
The person asking the question wanted to know why alt text for images aren’t returned in responses from the Sitecore Item Web API, and was curious if it were possible to include these.
After digging around the Sitecore.ItemWebApi.dll and my local copy of /App_Config/Include/Sitecore.ItemWebApi.config — this config file defines a bunch of pipelines and their processors that can be augmented or overridden — I learned field values are returned via logic in the Sitecore.ItemWebApi.Pipelines.Read.GetResult class, which is exposed in /configuration/sitecore/pipelines/itemWebApiRead/processor[@type=”Sitecore.ItemWebApi.Pipelines.Read.GetResult, Sitecore.ItemWebApi”] in /App_Config/Include/Sitecore.ItemWebApi.config:
This is an example of a raw value for an image field — it does not include the alt text for the image:
I spun up a copy of the console application written by Kern Herskind Nightingale — Director of Technical Services at Sitecore UK — to show the value returned by the above pipeline processor for an image field:
The Sitecore.ItemWebApi.Pipelines.Read.GetResult class exposes a virtual method hook — the protected method GetFieldInfo() — that allows custom code to change a field’s value before it is returned.
I wrote the following class as an example for changing an image field’s value:
using System; using System.IO; using System.Web; using System.Web.UI; using Sitecore.Data.Fields; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.ItemWebApi; using Sitecore.ItemWebApi.Pipelines.Read; using Sitecore.Web.UI.WebControls; using HtmlAgilityPack; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Read { public class EnsureImageFieldAltText : GetResult { protected override Dynamic GetFieldInfo(Field field) { Assert.ArgumentNotNull(field, "field"); Dynamic dynamic = base.GetFieldInfo(field); AddAltTextForImageField(dynamic, field); return dynamic; } private static void AddAltTextForImageField(Dynamic dynamic, Field field) { Assert.ArgumentNotNull(dynamic, "dynamic"); Assert.ArgumentNotNull(field, "field"); if(IsImageField(field)) { dynamic["Value"] = AddAltTextToImages(field.Value, GetAltText(field)); } } private static string AddAltTextToImages(string imagesXml, string altText) { if (string.IsNullOrWhiteSpace(imagesXml) || string.IsNullOrWhiteSpace(altText)) { return imagesXml; } HtmlDocument htmlDocument = new HtmlDocument(); htmlDocument.LoadHtml(imagesXml); HtmlNodeCollection images = htmlDocument.DocumentNode.SelectNodes("//image"); foreach (HtmlNode image in images) { if (image.Attributes["src"] != null) { image.SetAttributeValue("src", GetAbsoluteUrl(image.GetAttributeValue("src", string.Empty))); } image.SetAttributeValue("alt", altText); } return htmlDocument.DocumentNode.InnerHtml; } private static string GetAbsoluteUrl(string url) { Assert.ArgumentNotNullOrEmpty(url, "url"); Uri uri = HttpContext.Current.Request.Url; if (url.StartsWith(uri.Scheme)) { return url; } string port = string.Empty; if (uri.Port != 80) { port = string.Concat(":", uri.Port); } return string.Format("{0}://{1}{2}/~{3}", uri.Scheme, uri.Host, port, VirtualPathUtility.ToAbsolute(url)); } private static string GetAltText(Field field) { Assert.ArgumentNotNull(field, "field"); if (IsImageField(field)) { ImageField imageField = field; if (imageField != null) { return imageField.Alt; } } return string.Empty; } private static bool IsImageField(Field field) { Assert.ArgumentNotNull(field, "field"); return field.Type == "Image"; } } }
The class above — with the help of the Sitecore.Data.Fields.ImageField class — gets the alt text for the image, and adds a new alt XML attribute to the XML before it is returned.
The class also changes the relative url defined in the src attribute in to be an absolute url.
I then swapped out /configuration/sitecore/pipelines/itemWebApiRead/processor[@type=”Sitecore.ItemWebApi.Pipelines.Read.GetResult, Sitecore.ItemWebApi”] with the class above in /App_Config/Include/Sitecore.ItemWebApi.config:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <!-- Lots of stuff here --> <!-- Handles the item read operation. --> <itemWebApiRead> <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Read.EnsureImageFieldAltText, Sitecore.Sandbox" /> </itemWebApiRead> <!--Lots of stuff here too --> </pipelines> <!-- Even more stuff here --> </sitecore> </configuration>
I then reran the console application to see what the XML now looks like, and as you can see the new alt attribute was added:
You might be thinking “Mike, image field XML values are great in Sitecore’s Content Editor, but client code consuming this data might have trouble with it. Is there anyway to have HTML be returned instead of XML?
You bet!
The following subclass of Sitecore.ItemWebApi.Pipelines.Read.GetResult returns HTML, not XML:
using System; using System.IO; using System.Web; using System.Web.UI; using Sitecore.Data.Fields; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.ItemWebApi; using Sitecore.ItemWebApi.Pipelines.Read; using Sitecore.Web.UI.WebControls; using HtmlAgilityPack; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Read { public class TailorFieldValue : GetResult { protected override Dynamic GetFieldInfo(Field field) { Assert.ArgumentNotNull(field, "field"); Dynamic dynamic = base.GetFieldInfo(field); TailorValueForImageField(dynamic, field); return dynamic; } private static void TailorValueForImageField(Dynamic dynamic, Field field) { Assert.ArgumentNotNull(dynamic, "dynamic"); Assert.ArgumentNotNull(field, "field"); if (field.Type == "Image") { dynamic["Value"] = SetAbsoluteUrlsOnImages(GetImageHtml(field)); } } private static string SetAbsoluteUrlsOnImages(string html) { if (string.IsNullOrWhiteSpace(html)) { return html; } HtmlDocument htmlDocument = new HtmlDocument(); htmlDocument.LoadHtml(html); HtmlNodeCollection images = htmlDocument.DocumentNode.SelectNodes("//img"); foreach (HtmlNode image in images) { if (image.Attributes["src"] != null) { image.SetAttributeValue("src", GetAbsoluteUrl(image.GetAttributeValue("src", string.Empty))); } } return htmlDocument.DocumentNode.InnerHtml; } private static string GetAbsoluteUrl(string url) { Assert.ArgumentNotNullOrEmpty(url, "url"); Uri uri = HttpContext.Current.Request.Url; if (url.StartsWith(uri.Scheme)) { return url; } string port = string.Empty; if (uri.Port != 80) { port = string.Concat(":", uri.Port); } return string.Format("{0}://{1}{2}{3}", uri.Scheme, uri.Host, port, VirtualPathUtility.ToAbsolute(url)); } private static string GetImageHtml(Field field) { return GetImageHtml(field.Item, field.Name); } private static string GetImageHtml(Item item, string fieldName) { Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNullOrEmpty(fieldName, "fieldName"); return RenderImageControlHtml(new Image { Item = item, Field = fieldName }); } private static string RenderImageControlHtml(Image image) { Assert.ArgumentNotNull(image, "image"); string html = string.Empty; using (TextWriter textWriter = new StringWriter()) { using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(textWriter)) { image.RenderControl(htmlTextWriter); } html = textWriter.ToString(); } return html; } } }
The class above uses an instance of the Image field control (Sitecore.Web.UI.WebControls.Image) to do all the work for us around building the HTML for the image, and we also make sure the url within it is absolute — just as we had done above.
I then wired this up to my local Sitecore instance in /App_Config/Include/Sitecore.ItemWebApi.config:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <!-- Lots of stuff here --> <!-- Handles the item read operation. --> <itemWebApiRead> <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Read.TailorFieldValue, Sitecore.Sandbox" /> </itemWebApiRead> <!--Lots of stuff here too --> </pipelines> <!-- Even more stuff here --> </sitecore> </configuration>
I then executed the console application, and was given back HTML for the image:
If you can think of other reasons for manipulating field values in subclasses of Sitecore.ItemWebApi.Pipelines.Read.GetResult, please drop a comment.
Addendum
Kieran Marron — a Lead Developer at Sitecore — wrote another Sitecore.ItemWebApi.Pipelines.Read.GetResult subclass example that returns an image’s alt text in the Sitecore Item Web API response via a new JSON property. Check it out!