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

Add the HTML5 Range Input Control into Web Forms for Marketers in Sitecore

$
0
0

A couple of weeks ago, I was researching what new input controls exist in HTML5 — I am quite a dinosaur when it comes to front-end code, and felt it was a good idea to see what is currently available or possible — and discovered the range HTML control:

range-example

I immediately wanted to add this HTML5 input control into Web Forms for Marketers in Sitecore, and built the following control class as a proof of concept:

using System;
using System.Web.UI;
using System.Web.UI.WebControls;

using Sitecore.Diagnostics;
using Sitecore.Form.Web.UI.Controls;

namespace Sitecore.Sandbox.Form.Web.UI.Controls
{
    public class Range : InputControl
    {
        public Range() : this(HtmlTextWriterTag.Div)
        {
        }

        public Range(HtmlTextWriterTag tag)
            : base(tag)
        {
        }

        protected override void OnInit(EventArgs e)
        {
            base.textbox.CssClass = "scfSingleLineTextBox";
            base.help.CssClass = "scfSingleLineTextUsefulInfo";
            base.generalPanel.CssClass = "scfSingleLineGeneralPanel";
            base.title.CssClass = "scfSingleLineTextLabel";
            this.Controls.AddAt(0, base.generalPanel);
            this.Controls.AddAt(0, base.title);
            base.generalPanel.Controls.AddAt(0, base.help);
            base.generalPanel.Controls.AddAt(0, textbox);
        }

        protected override void DoRender(HtmlTextWriter writer)
        {
            textbox.Attributes["type"] = "range";
            TrySetIntegerAttribute("min", MinimumValue);
            TrySetIntegerAttribute("max", MaximumValue);
            TrySetIntegerAttribute("step", StepInterval);
            EnsureDefaultValue();
            textbox.MaxLength = 0;
            base.DoRender(writer);
        }

        protected virtual void TrySetIntegerAttribute(string attributeName, string value)
        {
            int integerValue;
            if (int.TryParse(value, out integerValue))
            {
                SetAttribute(attributeName, integerValue.ToString());
            }
        }

        protected virtual void SetAttribute(string attributeName, string value)
        {
            Assert.ArgumentNotNull(textbox, "textbox");
            Assert.ArgumentNotNullOrEmpty(attributeName, "attributeName");
            textbox.Attributes[attributeName] = value;
        }

        protected virtual void EnsureDefaultValue()
        {
            int value;
            if (!int.TryParse(Text, out value))
            {
                textbox.Text = string.Empty;
            }
        }

        public string MinimumValue { get; set; }

        public string MaximumValue { get; set; }

        public string StepInterval { get; set; }
    }
}

Most of the magic behind how this works occurs in the DoRender() method above. In that method we are changing the “type” attribute on the TextBox instance defined in the parent InputControl class to be “range” instead of “text”: this is how the browser will know that it is to render a range control instead of a textbox.

The DoRender() method also delegates to other helper methods: one to set the default value for the control, and another to add additional attributes to our control — the “step”, “min”, and “max” attributes in particular (you can learn more about these attributes by reading this specification for the range control) — and these are only set when values are passed to our code via XML defined in Sitecore for the control:

range-field-type

Let’s test this out!

I whipped up a test form, and added a range field to it:

range-form-test

This is what the form looked like on the page before I clicked the submit button (trust me, that’s 75 ;) ):

range-form-before-submit

After clicking submit, I was given a confirmation message:

range-form-after-submit

As you can see in the Form Reports for our test form, the value selected on the range control was captured:

range-form-reports

I will admit that I had a lot of fun adding this range input control into Web Forms for Marketers but do question whether anyone would use this control.

Why?

I found no way to add label markers for the different values on the control (if you are aware of a way to do this, please leave a comment).

Also, it should be noted that this control will not work in Internet Explorer 9 or earlier versions.

If you can think of ways around making this better, or have ideas for other HTML5 controls that could/should be added to Web Forms for Marketers, please share in a comment.



Restrict IP Access of Directories and Files in Your Sitecore Web Application Using a httpRequestBegin Pipeline Processor

$
0
0

Last week my friend and colleague Greg Coffman had asked me if I knew of a way to restrict IP access to directories within the Sitecore web application, and I recalled reading a post by Alex Shyba quite some time ago.

Although Alex’s solution is probably good enough in most circumstances, I decided to explore other solutions, and came up with the following <httpRequestBegin> pipeline processor as another way to accomplish this:

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

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.HttpRequest;
using Sitecore.Web;

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

            RedirectToNoAccessUrl();
        }

        private bool ShouldRedirect(HttpRequestArgs args)
        {
            return CanProcess(args, GetFilePath(args)) 
                    && !CanAccess(args.Context.Request.UserHostAddress);
        }

        protected virtual string GetFilePath(HttpRequestArgs args)
        {
            if (string.IsNullOrWhiteSpace(Context.Page.FilePath))
            {
                return args.Url.FilePath;
            }

            return Context.Page.FilePath;
        }

        protected virtual bool CanProcess(HttpRequestArgs args, string filePath)
        {
            return !string.IsNullOrWhiteSpace(filePath)
                    && !string.IsNullOrWhiteSpace(RootFilePath)
                    && AllowedIPs != null
                    && AllowedIPs.Any()
                    && (HostingEnvironment.VirtualPathProvider.DirectoryExists(filePath)
                        || HostingEnvironment.VirtualPathProvider.FileExists(filePath))
                    && args.Url.FilePath.StartsWith(RootFilePath, StringComparison.CurrentCultureIgnoreCase)
                    && !string.IsNullOrWhiteSpace(args.Context.Request.UserHostAddress)
                    && !string.Equals(filePath, Settings.NoAccessUrl, StringComparison.CurrentCultureIgnoreCase);
        }

        protected virtual bool CanAccess(string ip)
        {
            Assert.ArgumentNotNullOrEmpty(ip, "ip");
            return AllowedIPs.Contains(ip);
        }

        protected virtual void RedirectToNoAccessUrl()
        {
            WebUtil.Redirect(Settings.NoAccessUrl);
        }

        protected virtual void AddAllowedIP(string ip)
        {
            if (string.IsNullOrWhiteSpace(ip) || AllowedIPs.Contains(ip))
            {
                return;
            }

            AllowedIPs.Add(ip);
        }

        private string RootFilePath { get; set; }

        private IList<string> _AllowedIPs;
        private IList<string> AllowedIPs 
        {
            get
            {
                if (_AllowedIPs == null)
                {
                    _AllowedIPs = new List<string>();
                }

                return _AllowedIPs;
            }
        }
    }
}

The pipeline processor above determines whether the IP making the request has access to the directory or file on the file system — a list of IP addresses that should have access are passed to the pipeline processor via a configuration file, and the code does check to see if the requested URL is a directory or a file on the file system — by matching the beginning of the URL with a configuration defined root path.

If the user does not have access to the requested path, s/he is redirected to the “No Access Url” which is specified in the Sitecore instance’s configuration.

The list of IP addresses that should have access to the directory — including everything within it — and the root path are handed to the pipeline processor via the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor patch:before=" processor[@type='Sitecore.Pipelines.HttpRequest.FileResolver, Sitecore.Kernel']"
                   type="Sitecore.Sandbox.Pipelines.HttpRequest.FilePathRestrictor, Sitecore.Sandbox">
          <RootFilePath>/sitecore</RootFilePath>
          <AllowedIPs hint="list:AddAllowedIP">
            <IP>127.0.0.2</IP>
          </AllowedIPs>
        </processor>
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

Since my IP is 127.0.0.1, I decided to only allow 127.0.0.2 access to my Sitecore directory — this also includes everything within it — in the above configuration file for testing.

After navigating to /sitecore of my local sandbox instance, I was redirected to the “No Access Url” page defined in my Web.config:

no-access

If you have any thoughts on this, or know of other solutions, please share in a comment.


Export to CSV in the Form Reports of Sitecore’s Web Forms for Marketers

$
0
0

The other day I was poking around Sitecore.Forms.Core.dll — this is one of the assemblies that comes with Web Forms for Marketers (what, you don’t randomly look at code in the Sitecore assemblies? ;) ) — and decided to check out how the export functionality of the Form Reports work.

Once I felt I understood how the export code functions, I decided to take a stab at building my own custom export: functionality to export to CSV, and built the following class to serve as a pipeline processor to wedge Form Reports data into CSV format:

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

using Sitecore.Diagnostics;
using Sitecore.Form.Core.Configuration;
using Sitecore.Form.Core.Pipelines.Export;
using Sitecore.Forms.Data;
using Sitecore.Jobs;

namespace Sitecore.Sandbox.Form.Core.Pipelines.Export.Csv
{
    public class ExportToCsv
    {
        public void Process(ExportArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            LogInfo();
            args.Result = GenerateCsv(args.Packet.Entries);
        }

        protected virtual void LogInfo()
        {
            Job job = Context.Job;
            if (job != null)
            {
                job.Status.LogInfo(ResourceManager.Localize("EXPORTING_DATA"));
            }
        }

        private string GenerateCsv(IEnumerable<IForm> forms)
        {
            return string.Join(Environment.NewLine, GenerateAllCsvRows(forms));
        }

        protected virtual IEnumerable<string> GenerateAllCsvRows(IEnumerable<IForm> forms)
        {
            Assert.ArgumentNotNull(forms, "forms");
            IList<string> rows = new List<string>();
            rows.Add(GenerateCsvHeader(forms.FirstOrDefault()));
            foreach (IForm form in forms)
            {
                string row = GenerateCsvRow(form);
                if (!string.IsNullOrWhiteSpace(row))
                {
                    rows.Add(row);
                }
            }

            return rows;
        }

        protected virtual string GenerateCsvHeader(IForm form)
        {
            Assert.ArgumentNotNull(form, "form");
            return string.Join(",", form.Field.Select(field => field.FieldName));
        }

        protected virtual string GenerateCsvRow(IForm form)
        {
            Assert.ArgumentNotNull(form, "form");
            return string.Join(",", form.Field.Select(field => field.Value));
        }
    }
}

There really isn’t anything magical happening in the code above. The code creates a string of comma-separated values for each row of entries in args.Packet.Entries, and puts these plus a CSV header into a collection of strings.

Once all rows have been placed into a collection of strings, they are munged together on the newline character ultimately creating a multi-row CSV string. This CSV string is then set on the Result property of the ExportArgs instance.

Now we need a way to invoke a pipeline that contains the above class as a processor, and the following command does just that:

using System.Collections.Specialized;

using Sitecore.Diagnostics;
using Sitecore.Forms.Core.Commands.Export;
using Sitecore.Form.Core.Configuration;
using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Forms.Core.Commands.Export
{
    public class Export : ExportToXml
    {
        protected override void AddParameters(NameValueCollection parameters)
        {
            parameters["filename"] = FileName;
            parameters["contentType"] = MimeType;
        }

        public override void Execute(CommandContext context)
        {
            SetProperties(context);
            base.Execute(context);
        }

        private void SetProperties(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Parameters, "context.Parameters");
            Assert.ArgumentNotNullOrEmpty(context.Parameters["fileName"], "context.Parameters[\"fileName\"]");
            Assert.ArgumentNotNullOrEmpty(context.Parameters["mimeType"], "context.Parameters[\"mimeType\"]");
            Assert.ArgumentNotNullOrEmpty(context.Parameters["exportPipeline"], "context.Parameters[\"exportPipeline\"]");
            Assert.ArgumentNotNullOrEmpty(context.Parameters["progressDialogTitle"], "context.Parameters[\"progressDialogTitle\"]");
            FileName = context.Parameters["fileName"];
            MimeType = context.Parameters["mimeType"];
            ExportPipeline = context.Parameters["exportPipeline"];
            ProgressDialogTitle = context.Parameters["progressDialogTitle"];
        }

        protected override string GetName()
        {
            return ProgressDialogTitle;
        }

        protected override string GetProcessorName()
        {
            return ExportPipeline;
        }

        private string FileName { get; set; }

        private string MimeType { get; set; }

        private string ExportPipeline { get; set; }

        private string ProgressDialogTitle { get; set; }
    }
}

I modeled the above command after Sitecore.Forms.Core.Commands.Export.ExportToExcel in Sitecore.Forms.Core.dll: this command inherits some useful logic of Sitecore.Forms.Core.Commands.Export.ExportToXml but differs along the pipeline being invoked, the name of the export file, and content type of the file being created.

I decided to make the above command be generic: the name of the file, pipeline, progress dialog title — this is a heading that is displayed in a modal dialog that is launched when the data is being exported from the Form Reports — and content type of the file are passed to it from Sitecore via Sheer UI buttons (see below).

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

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="forms:export" type="Sitecore.Sandbox.Forms.Core.Commands.Export.Export, Sitecore.Sandbox" />
    </commands>
    <pipelines>
      <exportToCsv>
        <processor type="Sitecore.Sandbox.Form.Core.Pipelines.Export.Csv.ExportToCsv, Sitecore.Sandbox" />
        <processor type="Sitecore.Form.Core.Pipelines.Export.SaveContent, Sitecore.Forms.Core" />
      </exportToCsv>
    </pipelines>
  </sitecore>
</configuration>

Now we must wire the command to Sheer UI buttons. This is how I wired up the export ‘All’ button (this button is available in a dropdown of the main export button in the Form Reports):

to-csv-all-core-db

I then created another export button which is used when exporting selected rows in the Form Reports:

to-csv-core-db

Let’s see this in action!

I opened up the Form Reports for a test form I had built for a previous blog post, and selected some rows (notice the ‘To CSV’ button in the ribbon):

form-reports-selected-export-csv

I clicked the ‘To CSV’ button — doing this launched a progress dialog (I wasn’t fast enough to grab a screenshot of it) — and was prompted to download the following file:

export-csv-txt

As you can see, the file looks beautiful in Excel ;) :

export-csv-excel

If you have any thoughts on this, or ideas for other export data formats that could be incorporated into the Form Reports of Web Forms for Marketers, please share in a comment.

Until next time, have a Sitecoretastic day!


Add ‘Has Content In Language’ Property to Sitecore Item Web API Responses

$
0
0

The other day I had read a forum thread on SDN where the poster had asked whether one could determine if content returned from the Sitecore Item Web API for an Item was the actual content for the Item in the requested language.

I was intrigued by this question because I would have assumed that no results would be returned for the Item when it does not have content in the requested language but that is not the case: I had replicated what the poster had seen.

As a workaround, I built the following class to serve as an <itemWebApiGetProperties> pipeline processor which sets a property in the response indicating whether the Item has content in the requested language (check out my previous post on adding additional properties to Sitecore Item Web API responses for more information on this topic):

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

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.ItemWebApi.Pipelines.GetProperties;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.GetProperties
{
    public class SetHasContentInLanguageProperty : GetPropertiesProcessor
    {
        public override void Process(GetPropertiesArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            Assert.ArgumentNotNull(arguments.Item, "arguments.Item");
            arguments.Properties.Add("HasContentInLanguage", IsLanguageInCollection(arguments.Item.Languages, arguments.Item.Language));
        }

        private static bool IsLanguageInCollection(IEnumerable<Language> languages, Language language)
        {
            Assert.ArgumentNotNull(languages, "languages");
            Assert.ArgumentNotNull(language, "language");
            return languages.Any(lang => lang == language);
        }
    }
}

The code in the above class checks to see if the Item has content in the requested language — the latter is set in the Language property of the Item instance, and the Languages property contains a list of all languages it has content for.

I then added the above pipeline processor via the following configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <itemWebApiGetProperties>
        <processor patch:after="processor[@type='Sitecore.ItemWebApi.Pipelines.GetProperties.GetProperties, Sitecore.ItemWebApi']"
            type="Sitecore.Sandbox.ItemWebApi.Pipelines.GetProperties.SetHasContentInLanguageProperty, Sitecore.Sandbox" />
      </itemWebApiGetProperties>
    </pipelines>
  </sitecore>
</configuration>

Let’s see how this works!

I first created an Item for testing:

content-in-language-test

This Item only has content in English:

content-in-language-test-en

I then toggled my Sitecore Item Web API configuration to allow for anonymous access so that I can make requests in my browser, and made a request for the test Item in English:

english-has-content

The Item does have content in English, and this is denoted by the ‘HasContentInLanguage’ property.

I then made a request for the Item in French:

french-does-not-have-content

As expected, the ‘HasContentInLanguage’ is false since the Item does not have content in French.

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


Launch PowerShell Scripts in the Item Context Menu using Sitecore PowerShell Extensions

$
0
0

Last week during my Sitecore PowerShell Extensions presentation at the Sitecore User Group Conference 2014 — a conference held in Utrecht, Netherlands — I demonstrated how to invoke PowerShell scripts from the Item context menu in Sitecore, and felt I should capture what I had shown in a blog post — yes, this is indeed that blog post. ;)

During that piece of my presentation, I shared the following PowerShell script to expands tokens in fields of a Sitecore item (if you want to learn more about tokens in Sitecore, please take a look at John West’s post about them, and also be aware that one can also invoke the Expand-Token PowerShell command that comes with Sitecore PowerShell Extensions to expand tokens on Sitecore items — this makes things a whole lot easier ;) ):

$item = Get-Item .
$tokenReplacer = [Sitecore.Configuration.Factory]::GetMasterVariablesReplacer()
$item.Editing.BeginEdit()
$tokenReplacer.ReplaceItem($item)
$item.Editing.EndEdit()
Close-Window

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

Once the Item has been processed, it is taken out of editing mode.

So how do we save this script so that we can use it in the Item context menu? The following screenshot walks you through the steps to do just that:

item-context-menu-powershell-ise

The script is saved to an Item created by the dialog above:

expand-tokens-item

Let’s test this out!

I selected an Item with unexpanded tokens:

home-tokens-to-expand

I then launched its Item context menu, and clicked the option we created to ‘Expand Tokens’:

home-item-context-menu-expand-tokens

As you can see the tokens were expanded:

home-tokens-expanded

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

Until next time, have a scriptolicious day ;)


Create a Custom Report in Sitecore PowerShell Extensions

$
0
0

During my Sitecore PowerShell Extensions presentation at the Sitecore User Group Conference 2014, I showcased a custom report I had scripted using the Sitecore PowerShell Extensions module, and thought I would jot down what I had shown coupled with some steps on how you could go about creating your own custom report.

I had shown the audience the following PowerShell script:

<#
    .SYNOPSIS
        Lists all images with an empty Alt field.
    
    .NOTES
        Mike Reynolds
#>

function Get-ImageItemNoAltText {    
    $items = Get-ChildItem -Path "master:\sitecore\media library\images" -Recurse | Where-Object { $_.Fields["Alt"] -ne $null }
    
    foreach($item in $items) {
        if($item."Alt" -eq '') {
            $item
        }
    }
}

$items = Get-ImageItemNoAltText

if($items.Count -eq 0) {
    Show-Alert "There are no images with an empty Alt field."
} else {
    $props = @{
        InfoTitle = "Images with an empty Alt field"
        InfoDescription = "Lists all images with an empty Alt field."
        PageSize = 25
    }
    
    $items |
        Show-ListView @props -Property @{Label="Name"; Expression={$_.DisplayName} },
            @{Label="Updated"; Expression={$_.__Updated} },
            @{Label="Updated by"; Expression={$_."__Updated by"} },
            @{Label="Created"; Expression={$_.__Created} },
            @{Label="Created by"; Expression={$_."__Created by"} },
            @{Label="Path"; Expression={$_.ItemPath} }
}
Close-Window

I modeled the above script after the “out of the box” ‘Unused media items’ report but made some changes: it grabs all media library items recursively under /sitecore/Media Library/Images — you could definitely change this to /sitecore/Media Library to get all images outside of the Images folder — in Sitecore that have an Alt field, and that Alt field’s value is equal to the empty string.

I then tested — yes, I do test my code, don’t you ;) — and saved my report using the PowerShell ISE:

custom-spe-report-images-no-alt-text

The report was saved in this Item created just for it:

custom-spe-report-images-no-alt-text-location

Let’s see this in action!

I went to Sitecore –> Reporting Tools –> PowerShell Reports –> Mikes Media Audit, and clicked on the new report:

went-to-custom-report

After running the report, I was presented with this dialog containing the results:

images-with-no-alt-text-report-results

I then clicked on the first row of the report, and was brought to an image with an empty Alt field:

go-to-an-image-no-alt-text

If you have any thoughts on this, or would like to see additional reports in Sitecore PowerShell Extensions, please share in a comment.


Build a Custom Command in Sitecore PowerShell Extensions

$
0
0

In my Sitecore PowerShell Extensions presentation at the Sitecore User Group Conference 2014, I showed the audience how easy it is to build custom commands for Sitecore PowerShell Extensions, and thought it would be a good idea to distill what I had shown into a blog post for future reference. This blog post embodies that endeavor.

During my presentation, I shared an example of using the template method pattern for two commands using the following base class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;

using Sitecore.Data.Items;

using Cognifide.PowerShell.PowerShellIntegrations.Commandlets;

namespace Sitecore.Sandbox.SPE.Commandlets.Data
{
    public abstract class EditItemCommand : BaseCommand
    {
        protected override void ProcessRecord()
        {
            ProcessItem(Item);
            if (!Recurse.IsPresent)
            {
                return;
            }
            
            ProcessItems(Item.Children, true);
        }

        private void ProcessItems(IEnumerable<Item> items, bool recursive)
        {
            foreach (Item item in items)
            {
                ProcessItem(item);
                if (recursive && item.Children.Any())
                {
                    ProcessItems(item.Children, recursive);
                }
            }
        }

        private void ProcessItem(Item item)
        {
            item.Editing.BeginEdit();
            try
            {
                EditItem(item);
                item.Editing.EndEdit();
            }
            catch (Exception exception)
            {
                item.Editing.CancelEdit();
                throw exception;
            }

            WriteItem(item);
        }

        protected abstract void EditItem(Item item);

        [Parameter(ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
        public Item Item { get; set; }

        [Parameter]
        public SwitchParameter Recurse { get; set; }
    }
}

The class above defines the basic algorithm for editing an Item — the editing part occurs in the EditItem() method which must be defined by subclasses — and all of its descendants when the Recurse switch is supplied to the command. When the Recursive switch is supplied, recursion is employed to process all descendants of the Item once editing of the supplied Item is complete.

The following subclass of the EditItemCommand class above protects a supplied Item in its implementation of the EditItem() method:

using System;
using System.Management.Automation;

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.SPE.Commandlets.Data
{
    [OutputType(new Type[] { typeof(Sitecore.Data.Items.Item) }), Cmdlet("Protect", "Item")]
    public class ProtectItemCommand : EditItemCommand
    {
        protected override void EditItem(Item item)
        {
            item.Appearance.ReadOnly = true;
        }
    }
}

Conversely, the following subclass of the EditItemCommand class unprotects the passed Item in its EditItem() method implementation:

using System;
using System.Management.Automation;

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.SPE.Commandlets.Data
{
    [OutputType(new Type[] { typeof(Sitecore.Data.Items.Item) }), Cmdlet("Unprotect", "Item")]
    public class UnprotectItemCommand : EditItemCommand
    {
        protected override void EditItem(Item item)
        {
            item.Appearance.ReadOnly = false;
        }
    }
}

The verb and noun for each command is defined in the Cmdlet class attribute set on each command class declaration.

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

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <powershell>
      <commandlets>
            <add Name="Sitecore Sandbox Commandlets" type="*, Sitecore.Sandbox" />
      </commandlets>
    </powershell>
  </sitecore>                                                                                    
</configuration>

Since everything looks copacetic — you got to love a developer’s optimism ;) — I built and deployed all of the above to my Sitecore instance.

Let’s take this for a spin!

I selected my home Item knowing it is not protected:

not-protected-home

I then looked to see if it had an unprotected descendant, and found the following item:

not-protected-page-three

I then ran a script on the home Item using our new command to protect an item, and supplied the Recurse switch to protect all descendants:

protect-item-command-powershell-ise

As you can see, the home Item is now protected:

protected-home

Its descendant is also protected:

protected-page-three

Let’s now unprotect them. I ran a script on the home Item using our new command to unprotect an item, and supplied the Recurse switch to process all descendants:

unprotect-item-command-powershell-ise

As you can see, the home Item is now unprotected again:

not-protected-again-home

Its descendant is also unprotected:

not-protected-again-page-three

If you have any thoughts or ideas around improving anything you’ve seen in this post, or have other ideas for commands that should be included in Sitecore PowerShell Extensions, please drop a comment.

I would also like to point out that I had written a previous blog post on creating a custom command in Sitecore PowerShell Extensions. You might want to go check that out as well.

Until next time, have a scriptastic day! :)


Execute PowerShell Scripts in Scheduled Tasks using Sitecore PowerShell Extensions

$
0
0

At the Sitecore User Group Conference 2014, I demonstrated how to invoke PowerShell scripts in a Sitecore Scheduled Task using Sitecore PowerShell Extensions, and felt I should pen what I had shown in a blog post — yes, you guessed it: this is that blog post. ;)

In my presentation, I shared the following PowerShell script with the audience:

ForEach($site in [Sitecore.Configuration.Factory]::GetSiteNames()) {
    $siteInfo = [Sitecore.Configuration.Factory]::GetSiteInfo($site)
    if($siteInfo -ne $null) {
         $siteInfo.HtmlCache.Clear()   
         $logEntry = [string]::Format("HtmlCache.Clear() invoked for {0}", $siteInfo.Name)
         Write-Log $logEntry
    }
}

The script above iterates over all sites in your Sitecore instance, clears the Html Cache for each, and creates log entries expressing the Html Cache was cleared for all sites processed.

You would probably never have to use a script like the one above. I only wrote it for demonstration purposes since I couldn’t think of a more practical example to show. If you can think of any practical examples, or feel the script above has some practicality, please share in a comment.

I wrote, tested, and saved the above script in the PowerShell ISE:

powershell-ise-task

The PowerShell script was saved to a new Item created by the dialog above:

task-location-script-library

I then created a Schedule Item to invoke the script housed in the Item above (to learn more about Sitecore Scheduled Tasks, please see John West‘s post discussing them):

create-schedule-item-spe

I saved my Item, waited a bit, and opened up my latest Sitecore log file:

html-cache-clear-spe

As you can see, the Html Cache was cleared for each site in my Sitecore instance.

If you have any questions/comments/thoughts on this, please drop a comment.



Make Bulk Item Updates using Sitecore PowerShell Extensions

$
0
0

In my Sitecore PowerShell Extensions presentation at the Sitecore User Group Conference 2014, I demonstrated how simple it is to make bulk Item updates — perform the same update to multiple Sitecore items — using a simple PowerShell script, and thought I would write down what I had shown.

Sadly, I do not remember which script I had shared with the audience — the scratchpad text file I referenced during my presentation contains multiple scripts for making bulk updates to Items (if you attended my talk, and remember exactly what I had shown, please drop a comment).

Since I cannot recall which script I had shown — please forgive me ;) — let’s look at the following PowerShell script (this might be the script I had shown):

@(Get-Item .) + (Get-ChildItem -r .) | ForEach-Object { Expand-Token $_ }

This script grabs the context Item — this is denoted by a period — within the PowerShell ISE via the Get-Item command, and puts it into an array so that we can concatenate it with an array of all of its descendants — this is returned by the Get-ChildItem command with the -r parameter (r stands for recursive). The script then iterates over all Items in the resulting array, passes each to the Expand-Token command — this command is offered “out of the box” in Sitecore PowerShell Extensions — which expands tokens in every field on the Item.

Let’s see this in action!

My home Item has some tokens in its Title field:

home-tokens

One of its descendants also has tokens in its Title field:

descendant-tokens

I opened up the PowerShell ISE, wrote my script, and executed:

powershell-ise-tokens

As you can see, the tokens on the home Item were expanded:

home-tokens-expanded

They were also expanded on the home Item’s descendant:

descendant-tokens-expanded

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


Show Submitted Web Forms for Marketers Form Field Values on a Confirmation Page in Sitecore

$
0
0

Recently on my About page, someone had asked me how to show submitted form field values in Web Forms for Marketers.

I had done such a thing in a past project, and thought I would share how I went about accomplishing this.

This solution reuses an instance of a storage class I had used in a previous post.

This is the interface for that storage class:

namespace Sitecore.Sandbox.Utilities.Storage
{
    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);
    }
}

This is the implementation of the storage class:

using System.Web;
using System.Web.SessionState;

using Sitecore.Diagnostics;

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

        public SessionRepository()
            : this(HttpContext.Current.Session)
        {
        }

        public SessionRepository(HttpSessionState session)
            : this(CreateNewHttpSessionStateWrapper(session))
        {
        }

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

The class above basically serializes a supplied object, and puts it into session using a key given by the calling code.

Plus, you can remove objects saved in it using a key.

I modified this class from the original version: I declared the constructors public so that I can reference them in a Sitecore configuration file (you will see this configuration file further down in this post).

I then created a POCO to house form field values for serialization purposes:

using System;
using System.Collections.Generic;

namespace Sitecore.Sandbox.Form.Submit.DTO
{
    [Serializable]
    public class Field
    {
        public string Name { get; set; }

        public string Value { get; set; }
    }
}

Field values belong to a form, so I built another POCO class to store a collection of Sitecore.Sandbox.Form.Submit.DTO.Field class instances, and also hold on to the submitted form’s ID:

using System;
using System.Collections.Generic;

namespace Sitecore.Sandbox.Form.Submit.DTO
{
    [Serializable]
    public class FormSubmission
    {
        public Guid ID { get; set; }

        public List<Field> Fields { get; set; }
    }
}

Now we need a custom Web Forms for Marketers SaveAction — all custom SaveActions must implement Sitecore.Form.Core.Client.Data.Submit.ISaveAction which is defined in Sitecore.Forms.Core.dll — to create and store instances of the POCO classes defined above:

using System.Collections.Generic;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Web;

using Sitecore.Form.Core.Client.Data.Submit;
using Sitecore.Form.Core.Controls.Data;
using Sitecore.Form.Submit;

using Sitecore.Sandbox.Form.Submit.DTO;
using Sitecore.Sandbox.Utilities.Storage;

namespace Sitecore.Sandbox.Form.Submit
{
    public class StoreForm : ISaveAction
    {
        static StoreForm()
        {
            RepositoryKey = Settings.GetSetting("RepositoryKey");
            Assert.IsNotNullOrEmpty(RepositoryKey, "RepositoryKey must be set in your configuration!");

            Repository = Factory.CreateObject("repository", true) as IRepository<string, object>;
            Assert.IsNotNull(Repository, "Repository must be set in your configuration!");
        }

        public void Execute(ID formid, AdaptedResultList fields, params object[] data)
        {
            StoreFormSubmission(formid, fields);
        }

        protected virtual void StoreFormSubmission(ID formid, AdaptedResultList fields)
        {
            FormSubmission form = CreateNewFormSubmission(formid, fields);
            Repository[GetRepositoryKey()] = form;
        }

        protected virtual FormSubmission CreateNewFormSubmission(ID formid, AdaptedResultList fields)
        {
            return new FormSubmission
            {
                ID = formid.Guid,
                Fields = CreateNewFields(fields)
            };
        }

        protected virtual List<Field> CreateNewFields(AdaptedResultList results)
        {
            Assert.ArgumentNotNull(results, "results");
            List<Field> fields = new List<Field>();
            foreach (AdaptedControlResult result in results)
            {
                fields.Add(new Field{ Name = result.FieldName, Value = result.Value });
            }

            return fields;
        }

        protected virtual string GetRepositoryKey()
        {
            return string.Concat(RepositoryKey, "_", WebUtil.GetSessionID());
        }

        private static string RepositoryKey { get; set; }
        
        private static IRepository<string, object> Repository { get; set; }
    }
}

The SaveAction above creates instances of the POCO classes above using the submitted form field values, and passes these to an instance of a IRepository: this is defined in the configuration file below jointly with a substring of the unique storage key (this is a concatenation of the key defined in the following configuration file and the user’s session ID to guarantee a unique key):

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <repository type="Sitecore.Sandbox.Utilities.Storage.SessionRepository" />
    <settings>
      <setting name="RepositoryKey" value="MyRepository"/>
    </settings>
  </sitecore>
</configuration>

I then registered the ISaveAction class above in Web Forms for Marketers:

store-form-save-action

I then wired it up to my test form:

add-store-form-to-form

For testing, I created the following sublayout — no, it’s not the prettiest code I have ever written but I needed something quick for testing — which I mapped to a confirmation page:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Form Submission Confirmation.ascx.cs" Inherits="Sandbox.layouts.sublayouts.FormSubmissionConfirmation" %>
<asp:Repeater ID="rptConfirmation" runat="server">
    <HeaderTemplate>
        <h2>What you gave us:</h2>
    </HeaderTemplate>
    <ItemTemplate>
        <%# Eval("Name") %>: <%# Eval("Value") %>
    </ItemTemplate>
    <SeparatorTemplate>
        <br />
    </SeparatorTemplate>
</asp:Repeater>

The following class serves as the code-behind for the sublayout:

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

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Sandbox.Form.Submit.DTO;
using Sitecore.Sandbox.Utilities.Storage;
using Sitecore.Web;

namespace Sandbox.layouts.sublayouts
{
    public partial class FormSubmissionConfirmation : System.Web.UI.UserControl
    {
        private static string RepositoryKey { get; set; }
        
        private static IRepository<string, object> Repository { get; set; }

        static FormSubmissionConfirmation()
        {
            RepositoryKey = Settings.GetSetting("RepositoryKey");
            Assert.IsNotNullOrEmpty(RepositoryKey, "RepositoryKey must be set in your configuration!");

            Repository = Factory.CreateObject("repository", true) as IRepository<string, object>;
            Assert.IsNotNull(Repository, "Repository must be set in your configuration!");
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            string key = GetRepositoryKey();
            FormSubmission submission = Repository.Get(key) as FormSubmission;
            Repository.Remove(key);
            if (submission == null || !submission.Fields.Any())
            {
               Visible = false;
                return;
            }
            
            rptConfirmation.DataSource = submission.Fields;
            rptConfirmation.DataBind();
        }

        protected virtual string GetRepositoryKey()
        {
            return string.Concat(RepositoryKey, "_", WebUtil.GetSessionID());
        }
    }
}

The code-behind above gets the FormSubmission instance from the IRepository instance defined in the configuration file shown above, and passes the Field POCO instances within it to a repeater.

Let’s see this in action!

I navigated to my test form, and filled it in:
filled-in-form

After submitting the form, I was redirected to my confirmation page. As you can see the form values I had entered are displayed:

form-confirmation

One thing to note: the solution above only works when your Web Forms for Marketers confirmation page is its own page, and you set your form to redirect to it after submitting the form.

If you have any thoughts on this, or know of other ways to show submitted Web Forms for Marketers form field values on a confirmation page, please share in a comment.


Restrict Certain Files from Being Uploaded Through Web Forms for Marketers Forms in Sitecore: an Alternative Approach

$
0
0

This past weekend I noticed the <formUploadFile> pipeline in Web Forms for Marketers (WFFM), and wondered whether I could create an alternative solution to the one I had shared in this post — I had built a custom WFFM field type to prevent certain files from being uploaded through WFFM forms.

After some tinkering, I came up the following class to serve as a <formUploadFile> pipeline processor:

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

using Sitecore.Diagnostics;

using Sitecore.Form.Core.Pipelines.FormUploadFile;

namespace Sitecore.Sandbox.Form.Pipelines.FormUploadFile
{
    public class CheckMimeTypesNotAllowed
    {
        static CheckMimeTypesNotAllowed()
        {
            MimeTypesNotAllowed = new List<string>();
        }

        public void Process(FormUploadFileArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            string mimeType = GetMimeType(args.File.FileName);
            if (IsMimeTypeAllowed(mimeType))
            {
                return;
            }

            throw new Exception(string.Format("Uploading a file with MIME type {0} is not allowed", mimeType));
        }

        protected virtual string GetMimeType(string fileName)
        {
            Assert.ArgumentNotNullOrEmpty(fileName, "fileName");
            return MimeMapping.GetMimeMapping(fileName);
        }

        protected virtual bool IsMimeTypeAllowed(string mimeType)
        {
            foreach (string mimeTypeNotAllowed in MimeTypesNotAllowed)
            {
                if (AreEqualIgnoreCase(mimeTypeNotAllowed, mimeType))
                {
                    return false;
                }    
            }

            return true;
        }

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

        private static void AddMimeTypeNotAllowed(string mimeType)
        {
            if (string.IsNullOrWhiteSpace(mimeType) || MimeTypesNotAllowed.Contains(mimeType))
            {
                return;
            }

            MimeTypesNotAllowed.Add(mimeType);
        }

        private static IList<string> MimeTypesNotAllowed { get; set; }
    }
}

The class above adds restricted MIME types to a list — these MIME types are defined in a configuration file shown below — and checks to see if the uploaded file’s MIME type is in the restricted list. If it is restricted, an exception is thrown with some information — throwing an exception in WFFM prevents the form from being submitted.

MIME types are inferred using System.Web.MimeMapping.GetMimeMapping(string fileName), a method that is new in .NET 4.5 (this solution will not work in older versions of .NET).

I then registered the class above as a <formUploadFile> pipeline processor via the following configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <formUploadFile>
        <processor patch:before="processor[@type='Sitecore.Form.Core.Pipelines.FormUploadFile.Save, Sitecore.Forms.Core']"
                   type="Sitecore.Sandbox.Form.Pipelines.FormUploadFile.CheckMimeTypesNotAllowed">
          <mimeTypesNotAllowed hint="list:AddMimeTypeNotAllowed">
            <mimeType>application/octet-stream</mimeType>
          </mimeTypesNotAllowed >
        </processor>  
      </formUploadFile>
    </pipelines>
  </sitecore>
</configuration>

In case you are wondering, the MIME type being passed to the processor in the configuration file is for executables.

Let’s see how we did. ;)

I whipped up a test form, and pulled it up in a browser:

form-for-testing

I then selected an executable:

select-executable

I saw this after an attempt to submit the form:

form-upload-exception

And my log file expressed why:

exception-in-log

I then copied a jpeg into my test folder, and selected it to be uploaded:

select-jpg

After clicking the submit button, I was given a nice confirmation message:

success-upload

There is one problem with the solution above that I would like to point out: it does not address the issue of file extensions being changed. I could not solve this problem since the MIME type of the file being uploaded cannot be determined from the Sitecore.Form.Core.Media.PostedFile instance set in the arguments object: there is no property for it. :(

If you know of another way to determine MIME types on Sitecore.Form.Core.Media.PostedFile instances, or have other ideas for restricting certain files from being uploaded through WFFM, please share in a comment.


Set Default Alternate Text on Images Uploaded to the Sitecore Media Library

$
0
0

For the past couple of years, I have been trying to come up with an idea for adding a custom <getMediaCreatorOptions> pipeline processor — this is no lie or exaggeration — but had not thought of any good reason to do so until today: I figured out that I could add a processor to set default alternate text on an image being uploaded into the Sitecore Media Library.

The following class contains code to serve as a <getMediaCreatorOptions> pipeline processor to set default alternate text on an image Item during upload:

using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetMediaCreatorOptions;

namespace Sitecore.Sandbox.Pipelines.GetMediaCreatorOptions
{
    public class SetDefaultAlternateTextIfNeed
    {
        public void Process(GetMediaCreatorOptionsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!string.IsNullOrWhiteSpace(args.Options.AlternateText))
            {
                return;
            }

            args.Options.AlternateText = GetAlternateText(args);
        }

        protected virtual string GetAlternateText(GetMediaCreatorOptionsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (string.IsNullOrWhiteSpace(args.Options.Destination) || args.Options.Destination.IndexOf("/") < 0)
            {
                return string.Empty;
            }

            int startofNameIndex = args.Options.Destination.LastIndexOf("/") + 1;
            return args.Options.Destination.Substring(startofNameIndex);
        }
    }
}

The code above will set the AlternateText property of the Options property of the GetMediaCreatorOptionsArgs instance when its not set: I set it to be the name of the Media Library Item by default — I extract this from the path destination of the Item.

I then registered the above class as a <getMediaCreatorOptions> pipeline processor in the following Sitecore configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getMediaCreatorOptions>
        <processor type="Sitecore.Sandbox.Pipelines.GetMediaCreatorOptions.SetDefaultAlternateTextIfNeed, Sitecore.Sandbox"/>
      </getMediaCreatorOptions>
    </pipelines>
  </sitecore>
</configuration>

Let’s try this out.

I went to my Media Library, and selected an image to upload:

selected-image-upload

During the upload, I did not specify its alternate text.

As you can see, it was given an alternate text value by default:

default-alt-text-set

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

But whatever you do, just don’t let this happen to you:

anxiety-cat


Accept All Notifications on Clones of an Item using a Custom Command in Sitecore

$
0
0

As I was walking along a beach near my apartment tonight, I thought “wouldn’t it be nifty to have a button in the Sitecore ribbon to accept all notifications on clones of an Item instead of having to accept these manually on each clone?”

I immediately returned home, and whipped up the following command class:

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

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

namespace Sitecore.Sandbox.Shell.Framework.Commands
{
    public class AcceptAllNotificationsOnClones : Command
    {
        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            IEnumerable<Item> clones = GetClonesWithNotifications(GetItem(context));
            if(!clones.Any())
            {
                return CommandState.Hidden;
            }

            return CommandState.Enabled;
        }

        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Item item = GetItem(context);
            IEnumerable<Item> clones = GetClonesWithNotifications(item);
            if(!clones.Any())
            {
                return;
            }

            foreach (Item clone in clones)
            {
                AcceptAllNotifications(item.Database.NotificationProvider, clone);
            }
        }

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

        protected virtual IEnumerable<Item> GetClonesWithNotifications(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            IEnumerable<Item> clones = item.GetClones();
            if(!clones.Any())
            {
                return new List<Item>();
            }
            
            IEnumerable<Item> clonesWithNotifications = GetClonesWithNotifications(item.Database.NotificationProvider, clones);
            if(!clonesWithNotifications.Any())
            {
                return new List<Item>();
            }

            return clonesWithNotifications;
        }

        protected virtual IEnumerable<Item> GetClonesWithNotifications(NotificationProvider notificationProvider, IEnumerable<Item> clones)
        {
            Assert.ArgumentNotNull(notificationProvider, "notificationProvider");
            Assert.ArgumentNotNull(clones, "clones");
            return (from clone in clones
                    let notifications = notificationProvider.GetNotifications(clone)
                    where notifications.Any()
                    select clone).ToList();
        }

        protected virtual void AcceptAllNotifications(NotificationProvider notificationProvider, Item clone)
        {
            Assert.ArgumentNotNull(notificationProvider, "notificationProvider");
            Assert.ArgumentNotNull(clone, "clone");
            foreach (Notification notification in notificationProvider.GetNotifications(clone))
            {
                notification.Accept(clone);
            }
        }
    }
}

The code in the command above ensures the command is only visible when the selected Item in the Sitecore content tree has clones, and those clones have notifications — this visibility logic is contained in the QueryState() method.

When the command is invoked — this happens through the Execute() method — all clones with notifications of the selected Item are retrieved, and iterated over — each are passed to the AcceptAllNotifications() method which contains logic to accept all notifications on them via the Accept() method on a NotificationProvider instance: this NotificationProvider instance comes from the source Item’s Database property.

I then registered the above command class in Sitecore using the following configuration file:

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

We need a way to invoke this command. I created a new button to go into the ‘Item Clones’ chunk in the ribbon:

accept-notifications-on-clones-button-core

Let’s take this for a test drive!

I first created some clones:

created-clones

I then changed a field value on one of those clones:

changed-field-value-on-clone

On the clone’s source Item, I changed the same field’s value with something completely different, and added a new child item — the new button appeared after saving the Item:

new-child-item-field-value-change

Now, the clone has notifications on it:

notifications-on-clone

I went back to the source Item, clicked the ‘Accept Notifications On Clones’ button in the ribbon, and navigated back to the clone:

notifications-accepted

As you can see, the notifications were accepted.

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


Chain Source and Clone Items Together in Sitecore Workflow

$
0
0

Two months ago, I worked on a project where I had to find a solution to chain source Items and their clones together in Sitecore workflow — don’t worry, the clone Items were “locked down” by being protected so content authors cannot make changes to content on the clones — the clones serve as content copies of their source Items for a multi-site solution in a single Sitecore instance.

After some research, a few mistakes — well, maybe more than a few ;) — and massive help from Oleg Burov, Escalation Engineer at Sitecore USA, I put together a subclass of Sitecore.Workflows.Simple.Workflow — this lives in Sitecore.Kernel.dll — similar to the following:

using Sitecore.Data.Items;
using Sitecore.Workflows;
using Sitecore.Workflows.Simple;

namespace Sitecore.Sandbox.Workflows.Simple
{
    public class ChainSourceClonesWorkflow : Workflow 
    {
        public ChainSourceClonesWorkflow(string workflowID, WorkflowProvider owner)
            : base(workflowID, owner)
        {

        }
        public override WorkflowResult Execute(string commandID, Item item, string comments, bool allowUI, params object[] parameters)
        {
            WorkflowResult result = base.Execute(commandID, item, comments, allowUI, parameters);
            foreach (Item clone in item.GetClones())
            {
                base.Execute(commandID, clone, comments, allowUI, parameters);
            }

            return result;
        }
    }
}

The Execute() method above basically moves the passed Item through to the next workflow state by calling the base class’ Execute() method, and grabs all clones for the passed Item — each are also pushed through to the next workflow state via the base class’ Execute() method.

Workflow instances are created by Sitecore.Workflows.Simple.WorkflowProvider. I created the following class to return an instance of the ChainSourceClonesWorkflow class above:

using Sitecore.Workflows;
using Sitecore.Workflows.Simple;

namespace Sitecore.Sandbox.Workflows.Simple
{
    public class ChainSourceClonesWorkflowProvider : WorkflowProvider
    {
        public ChainSourceClonesWorkflowProvider(string databaseName, HistoryStore historyStore)
            : base(databaseName, historyStore)
        {
        }

        protected override IWorkflow InstantiateWorkflow(string workflowId, WorkflowProvider owner)
        {
            return new ChainSourceClonesWorkflow(workflowId, owner);
        }
    }
}

I then replaced the “out of the box” WorkflowProvider with the one defined above using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <databases>
      <database id="master">
        <workflowProvider type="Sitecore.Workflows.Simple.WorkflowProvider, Sitecore.Kernel">
          <patch:attribute name="type">Sitecore.Sandbox.Workflows.Simple.ChainSourceClonesWorkflowProvider, Sitecore.Sandbox</patch:attribute>
        </workflowProvider>
      </database>
    </databases>
  </sitecore>
</configuration>

Let’s take this for a spin!

I first started with a source and clone in a “Draft” workflow state:

source-clone-draft

Let’s push the source — and hopefully clone ;) — through to the next workflow state by submitting it:

source-clone-submit

As you can see, both are “Awaiting Approval”:

source-clone-awaiting-approval

Let’s approve them:

source-clone-approve

As you can see, both are approved:

source-clone-approved

If you have any thoughts or comments on this, or know of ways to improve the code above, please drop a comment.

Also, keep in mind the paradigm above is not ideal when content authors are able to make content changes to clones which differ from their source Items. In that scenario, it would be best to let source and clone Items’ workflow be independent.


Omit HTML Breaks From Rendered Multi-Line Text Fields in Sitecore

$
0
0

Earlier today while preparing a training session on how to add JavaScript from a Multi-Line Text field to a rendered Sitecore page, I encountered something I had seen in the past but forgot about: Sitecore FieldControls and the FieldRenderer Web Control will convert newlines into HTML breaks.

For example, suppose you have the following JavaScript in a Multi-Line Text field:

javascript-in-field

You could use a Text FieldControl to render it:

<%@ Control Language="c#" AutoEventWireup="true" TargetSchema="http://schemas.microsoft.com/intellisense/ie5" %>
<sc:Text ID="scJavaScript" Field="JavaScript" runat="server" />

Unfortunately, your JavaScript will not work since it will contain HTML breaks:

html-breaks-in-rendered-value

Why does this happen? In the RenderField() method in Sitecore.Web.UI.WebControls.FieldRenderer — this lives in Sitecore.Kernel.dll, and is called by all FieldControls — passes a “linebreaks” parameter to the <renderField> pipeline:

field-renderer-html-breaks

The Process() method in Sitecore.Pipelines.RenderField.GetMemoFieldValue — this serves as one of the “out of the box” processors of the <renderField> pipeline — converts all carriage returns, line feeds, and newlines into HTML breaks:

get-memo-field-value

What can we do to prevent this from happening? Well, you could spin up a new class with a Process() method to serve as a new <renderField> pipeline processor, and use that instead of Sitecore.Pipelines.RenderField.GetMemoFieldValue:

using System;

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

namespace Sitecore.Sandbox.Pipelines.RenderField
{
    public class GetRawMemoFieldValueWhenApplicable
    {
        public void Process(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(!AreEqualIgnoreCase(args.FieldTypeKey, "memo") && !AreEqualIgnoreCase(args.FieldTypeKey, "multi-line text"))
            {
                return;
            }

            bool omitHtmlBreaks;
            if (bool.TryParse(args.Parameters["omitHtmlBreaks"], out omitHtmlBreaks))
            {
                return;
            }

            Assert.IsNotNull(DefaultGetMemoFieldValueProcessor, "DefaultGetMemoFieldValueProcessor must be set in your configuration!");
            DefaultGetMemoFieldValueProcessor.Process(args);
        }

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

        private GetMemoFieldValue DefaultGetMemoFieldValueProcessor { get; set; }
    }
}

The Process() method in the class above looks for an “omitHtmlBreaks” parameter, and just exits out of the Process() method when it is set to true — it leaves the field value “as is”.

If the “omitHtmlBreaks”parameter is not found in the RenderFieldArgs instance, or it is set to false, the Process() method delegates to the Process() method of its DefaultGetMemoFieldValueProcessor property — this would be an instance of the “out of the box” Sitecore.Pipelines.RenderField.GetMemoFieldValue, and this is passed to the new <renderField> pipeline processor via the following configuration file:

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

Let’s test this.

I added the “omitHtmlBreaks” parameter to the control I had shown above:

<%@ Control Language="c#" AutoEventWireup="true" TargetSchema="http://schemas.microsoft.com/intellisense/ie5" %>
<sc:Text ID="scJavaScript" Field="JavaScript" Parameters="omitHtmlBreaks=true" runat="server" />

When I loaded my page, I was given a warm welcome:

alert-box-welcome

When I viewed my page’s source, I no longer see HTML breaks:

javascript-no-html-breaks

If you have any thoughts on this, or know of another way to do this, please share in a comment.



Leverage the Sitecore Configuration Factory: Populate Class Properties with Instances of Types Defined in Configuration

$
0
0

I thought I would jot down some information that frequently comes up when I am asked to recommend plans of attack on projects. The first recommendation I always give for any Sitecore project is: define as much as you possibly can in Sitecore configuration. Doing so introduces seams in your code: code that does not have to change when its underlying behavior changes — think interfaces. ;)

When defining types in Sitecore configuration, you are leveraging Sitecore’s built-in Dependency Injection framework: Sitecore’s Configuration Factory will magically inject instances of classes — yes, you would define these in configuration files — into properties of classes that are used for your pipeline processors, event handlers, and other Sitecore configuration-defined objects.

For example, suppose we have the following interface for classes that change state on an instance of a phony subclass of Sitecore.Pipelines.PipelineArgs:

namespace Sitecore.Sandbox.Pipelines.SomePipeline
{
    public interface ISomeThing
    {
        void DoStuff(SomeProcessorArgs args);
    }
}

Let’s create a fake class that implements the ISomeThing interface above:

namespace Sitecore.Sandbox.Pipelines.SomePipeline
{
    public class SomeThing : ISomeThing
    {
        public void DoStuff(SomeProcessorArgs args)
        {
            // it would be nice if we had code in here
        }
    }
}

We can then define a class property with the ISomeThing interface type in a class that serves as a pipeline processor:

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.SomePipeline
{
    public class SomeProcessor
    {
        public void Process(SomeProcessorArgs args)
        {
            DoSomethingWithArgs(args);
        }

        private void DoSomethingWithArgs(SomeProcessorArgs args)
        {
            Assert.IsNotNull(SomeThing, "SomeThing must be set in your configuration!");
            SomeThing.DoStuff(args);
        }

        private ISomeThing SomeThing { get; set; } // this is populated magically!
    }
}

The class above would serve as a processor for the dummy <somePipeline> defined in the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <somePipeline>
        <processor type="Sitecore.Sandbox.Pipelines.SomePipeline.SomeProcessor, Sitecore.Sandbox">
          <SomeThing type="Sitecore.Sandbox.Pipelines.SomePipeline.SomeThing, Sitecore.Sandbox" />
        </processor>  
      </somePipeline>
    </pipelines>
  </sitecore>
</configuration>

In the configuration file above, we defined a <SomeThing /> element within the processor element of the <somePipeline> pipeline, and this directly maps to the SomeThing property in the SomeProcessor class shown above. Keep in mind that names matter here, so the name of the configuration element must match the name of the property.

Until next time, have a Sitecoretastic day!


Leverage the Sitecore Configuration Factory: Inject Dependencies Through Class Constructors

$
0
0

In my previous post, I showed how you can leverage Sitecore’s Configuration Factory to inject dependencies into properties of class instances — this is known as Setter injection in the Dependency injection world — and thought I would share another way you can inject dependencies into instances of classes defined in configuration: through class constructors (this is known as Constructor injection).

Suppose we have the following interface for objects that perform some kind of operation on parameters passed to their DoSomeOtherStuff() method:

namespace Sitecore.Sandbox.Pipelines.SomePipeline
{
    public interface ISomeOtherThing
    {
        void DoSomeOtherStuff(SomeProcessorArgs args, string someString);
    }
}

The following dummy class implements the interface above — sadly, it does not do anything:

namespace Sitecore.Sandbox.Pipelines.SomePipeline
{
    public class SomeOtherThing : ISomeOtherThing
    {
        public void DoSomeOtherStuff(SomeProcessorArgs args, string someString)
        {
            // TODO: add code to do some other stuff
        }
    }
}

In my previous post, I defined the following interface for objects that “do stuff”, and will reuse it here:

namespace Sitecore.Sandbox.Pipelines.SomePipeline
{
    public interface ISomeThing
    {
        void DoStuff(SomeProcessorArgs args);
    }
}

I have modified the SomeThing class from my previous post to consume an instance of a class that implements the ISomeOtherThing interface above along with a string instance:

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.SomePipeline
{
    public class SomeThing : ISomeThing
    {
        public SomeThing(ISomeOtherThing someOtherThing, string someString)
        {
            Assert.ArgumentNotNull(someOtherThing, "someOtherThing");
            Assert.ArgumentNotNullOrEmpty(someString, "someString");
            SomeOtherThing = someOtherThing;
            SomeString = someString;
        }

        public void DoStuff(SomeProcessorArgs args)
        {
            SomeOtherThing.DoSomeOtherStuff(args, SomeString);
        }

        private ISomeOtherThing SomeOtherThing { get; set; }

        private string SomeString { get; set; }
    }
}

The Sitecore Configuration Factory will magically create instances of the types passed to the constructor of the SomeThing class defined above, and you can then assign these instances to members defined in your class.

As far as I know — I did a lot of digging in Sitecore.Kernel.dll for answers, and some code experimentation — the Sitecore Configuration Factory will only magically inject instances of class types and strings: it will not inject .NET primitive types (if I am incorrect on this, please share in a comment).

I have reused the SomeProcessor class from my previous post — I did not change any code in it:

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.SomePipeline
{
    public class SomeProcessor
    {
        public void Process(SomeProcessorArgs args)
        {
            DoSomethingWithArgs(args);
        }

        private void DoSomethingWithArgs(SomeProcessorArgs args)
        {
            Assert.ArgumentNotNull(SomeThing, "SomeThing");
            SomeThing.DoStuff(args);
        }

        private ISomeThing SomeThing { get; set; } // is populated magically via Setter injection!
    }
}

We can then piece everything together using a Sitecore patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <somePipeline>
        <processor type="Sitecore.Sandbox.Pipelines.SomePipeline.SomeProcessor, Sitecore.Sandbox">
          <SomeThing type="Sitecore.Sandbox.Pipelines.SomePipeline.SomeThing, Sitecore.Sandbox">
            <param hint="1" type="Sitecore.Sandbox.Pipelines.SomePipeline.SomeOtherThing, Sitecore.Sandbox" />
            <param hint="2">just some string</param>
          </SomeThing>  
        </processor>  
      </somePipeline>
    </pipelines>
  </sitecore>
</configuration>

You must define <param> elements in order to pass arguments to constructors. The “hint” attribute determines the order of the parameters passed to the class constructor, though I believe using this attribute is optional (if I am wrong on this assumption, please share in a comment below).

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


Leverage the Sitecore Configuration Factory: Inject Value Types as Dependencies

$
0
0

In a previous post, I had mentioned primitive types cannot be injected into classes via Sitecore’s Configuration Factory, and further learned through exploring and experimentation that this applies to all value types in C#.

However, after some digging in Sitecore.Configuration.Factory — this lives in Sitecore.Kernel.dll — I discovered the CreateFromFactoryMethod() method which calls a static method — this method is defined in an XML attribute named “factoryMethod” — on a class defined in the “type” attribute on the XML configuration element being processed, and felt I could take advantage of this hidden gem for injecting value types into my configuration-defined classes.

Despite the fact the method defined in the “factoryMethod” attribute must be static, I decided to define an interface for classes that do value type conversions from strings, and created the following interface (don’t worry, I haven’t completely lost my mind — yes, I did say it requires a static method — but do bear with me until we get further down in this post ;) ):

namespace Sitecore.Sandbox.Configuration
{
    public interface IValueTypesConverter
    {
        bool ConvertToBoolean(string argument);

        char ConvertToCharacter(string argument);

        decimal ConvertToDecimal(string argument);

        double ConvertToDouble(string argument);

        float ConvertToFloat(string argument);

        int ConvertToInteger(string argument);
    }
}

I then created the following class to implement the interface above:

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Configuration
{
    public class ValueTypesConverter : IValueTypesConverter
    {
        public bool ConvertToBoolean(string argument)
        {
            bool value;
            Assert.ArgumentCondition(bool.TryParse(argument, out value), "argument", "argument is not a boolean!");
            return value;
        }

        public char ConvertToCharacter(string argument)
        {
            char value;
            Assert.ArgumentCondition(char.TryParse(argument, out value), "argument", "argument is not a character!");
            return value;
        }

        public decimal ConvertToDecimal(string argument)
        {
            decimal value;
            Assert.ArgumentCondition(decimal.TryParse(argument, out value), "argument", "argument is not a decimal!");
            return value;
        }

        public double ConvertToDouble(string argument)
        {
            double value;
            Assert.ArgumentCondition(double.TryParse(argument, out value), "argument", "argument is not a double!");
            return value;
        }

        public float ConvertToFloat(string argument)
        {
            float value;
            Assert.ArgumentCondition(float.TryParse(argument, out value), "argument", "argument is not a float!");
            return value;
        }

        public int ConvertToInteger(string argument)
        {
            int value;
            Assert.ArgumentCondition(int.TryParse(argument, out value), "argument", "argument is not an integer!");
            return value;
        }
    }
}

Each method in the class above calls its specific value type’s TryParse() method to convert the passed string to the value type. We ensure the argument was in the correct format: an assertion is done to check if the argument was of the type the method was expecting.

I then created the following adapter which wraps an instance of a class that implements the interface defined above:

using Sitecore.Configuration;

namespace Sitecore.Sandbox.Configuration
{
    public class TypesConverter
    {
        static TypesConverter()
        {
            ValueTypesConverter = Factory.CreateObject("valueTypesConverter", true) as IValueTypesConverter;
        }

        public static bool ConvertToBoolean(string argument)
        {
            return ValueTypesConverter.ConvertToBoolean(argument);
        }

        public static char ConvertToCharacter(string argument)
        {
            return ValueTypesConverter.ConvertToCharacter(argument);
        }

        public static decimal ConvertToDecimal(string argument)
        {
            return ValueTypesConverter.ConvertToDecimal(argument);
        }

        public static double ConvertToDouble(string argument)
        {
            return ValueTypesConverter.ConvertToDouble(argument);
        }

        public static float ConvertToFloat(string argument)
        {
            return ValueTypesConverter.ConvertToFloat(argument);
        }

        public static int ConvertToInteger(string argument)
        {
            return ValueTypesConverter.ConvertToInteger(argument);
        }

        private static IValueTypesConverter ValueTypesConverter { get; set; }
    }
}

The class above creates an instance of the ValueTypesConverter class via the Sitecore.Configuration.Factory.CreateObject() method. I felt defining the class that implements the IValueTypesConverter interface in Sitecore configuration would allow for customization if one would ever need to do so.

Further, all methods in the class above just delegate calls to the ValueTypesConverter instance.

For testing, I created the following class to serve as a <renderField> pipeline processor:

using System.Text;

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

namespace Sitecore.Sandbox.Pipelines.RenderField
{
    public class ValueTypesTest
    {
        public void Process(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            StringBuilder htmlBuilder = new StringBuilder();
            htmlBuilder.AppendFormat("SomeBoolean ==> {0} <br />", SomeBoolean);
            htmlBuilder.AppendFormat("SomeCharacter ==> {0} <br />", SomeCharacter);
            htmlBuilder.AppendFormat("SomeDecimal ==> {0} <br />", SomeDecimal);
            htmlBuilder.AppendFormat("SomeDouble ==> {0} <br />", SomeDouble);
            htmlBuilder.AppendFormat("SomeFloat ==> {0} <br />", SomeFloat);
            htmlBuilder.AppendFormat("SomeInteger ==> {0}", SomeInteger);
            args.Result.FirstPart = htmlBuilder.ToString();
        }

        private bool SomeBoolean { get; set; }

        private char SomeCharacter { get; set; }

        private decimal SomeDecimal { get; set; }

        private double SomeDouble { get; set; }

        private float SomeFloat { get; set; }

        private int SomeInteger { get; set; }
    }
}

The class above defines properties that will be populated magically by Sitecore’s Configuration Factory, and builds a string of HTML to be rendered on the front-end.

I wired everything together in the following Sitecore configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderField>
        <processor type="Sitecore.Sandbox.Pipelines.RenderField.ValueTypesTest, Sitecore.Sandbox">
          <SomeBoolean type="Sitecore.Sandbox.Configuration.TypesConverter, Sitecore.Sandbox" factoryMethod="ConvertToBoolean" arg0="true" />
          <SomeCharacter type="Sitecore.Sandbox.Configuration.TypesConverter, Sitecore.Sandbox" factoryMethod="ConvertToCharacter" arg0="b" />
          <SomeDecimal type="Sitecore.Sandbox.Configuration.TypesConverter, Sitecore.Sandbox" factoryMethod="ConvertToDecimal" arg0="0.1" />
          <SomeDouble type="Sitecore.Sandbox.Configuration.TypesConverter, Sitecore.Sandbox" factoryMethod="ConvertToDouble" arg0="1234.5678" />
          <SomeFloat type="Sitecore.Sandbox.Configuration.TypesConverter, Sitecore.Sandbox" factoryMethod="ConvertToFloat" arg0=".98765" />
          <SomeInteger type="Sitecore.Sandbox.Configuration.TypesConverter, Sitecore.Sandbox" factoryMethod="ConvertToInteger" arg0="2" />
        </processor>  
      </renderField>
    </pipelines>
    <valueTypesConverter type="Sitecore.Sandbox.Configuration.ValueTypesConverter, Sitecore.Sandbox" />
  </sitecore>
</configuration>

When I loaded my home page, I saw that all value types defined in configuration were accounted for:

value-types-test-rendered

If you have any thoughts on this, or ideas around implementing this in a different way, please share in a comment.


Restart the Sitecore Server Using a Custom FileWatcher

$
0
0

For a few months now, I’ve been contemplating potential uses for a custom Sitecore.IO.FileWatcher — this lives in Sitecore.Kernel.dll, and defines abstract methods to handle changes to files on the file system within your Sitecore web application — and finally came up with something: how about a FileWatcher that restarts the Sitecore server when a certain file is uploaded to a specific directory?

You might be thinking “why would I ever want use such a thing?” Well, suppose you need to restart the Sitecore server on one of your Content Delivery Servers immediately, but you do not have direct access to it, and the person who does has left for the week. What do you do?

The following FileWatcher might be one option for the scenario above (another option might be to make frantic phone calls to get the server restarted):

using System;

using Sitecore.Diagnostics;
using Sitecore.Install;
using Sitecore.IO;

namespace Sitecore.Sandbox.IO
{
    public class RestartServerWatcher : FileWatcher
    {
        public RestartServerWatcher()
            : base("watchers/restartServer")
        {
        }

        protected override void Created(string fullPath)
        {
            try
            {
                Log.Info(string.Format("Restart server file detected: {0}. Restarting the server.", fullPath), this);
                FileUtil.Delete(fullPath);
                Installer.RestartServer();
            }
            catch (Exception exception)
            {
                Log.Error("Error in RestartServerWatcher", exception, typeof(RestartServerWatcher));
            }
        }
        
        protected override void Deleted(string filePath)
        {
            return;
        }

        protected override void Renamed(string filePath, string oldFilePath)
        {
            return;
        }
    }
}

All of the magic occurs in the Created() method above — we do not care if the file is renamed or deleted. If the file is detected, the code in the Created() method logs information to the Sitecore log, deletes the file, and then initiates a Sitecore server restart.

I created the following patch configuration file for the RestartServerWatcher class above:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <watchers>
      <restartServer>
        <folder>/restart</folder>
        <filter>restart-server.txt</filter>
      </restartServer>
    </watchers>
  </sitecore>
</configuration>

Since FileWatchers are HttpModules, I had to register the RestartServerWatcher in the <system.webServer> section of my Web.config (this configuration element lives outside of the <sitecore> configuration element, and cannot be mapped via a Sitecore patch configuration file):

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- Lots of stuff up here -->
<system.webServer>
	<!-- Some stuff here -->
	<add type="Sitecore.Sandbox.IO.RestartServerWatcher, Sitecore.Sandbox" name="SitecoreRestartServerWatcher"/>
</system.webServer>
<!-- More stuff down here -->
</configuration>

For testing, I uploaded my target file into the target location via the Sitecore File Explorer to trigger a Sitecore server restart:

file-explorer-wizard-upload

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

restart-server-log-file

If you have any thoughts on this, or have other ideas for custom FileWatchers, please share in a comment.


Clone Items using the Sitecore Item Web API

$
0
0

Yesterday, I had the privilege to present with Ben Lipson and Jamie Michalski, both of Velir, on the Sitecore Item Web API at the New England Sitecore User Group — if you want to see us in action, check out the recording of our presentation!

Plus, my slides are available here!

During my presentation, I demonstrated how easy it is to customize the Sitecore Item API by adding a custom <itemWebApiRequest> pipeline processor, and a custom pipeline to handle a cloning request — for another example on adding a custom <itemWebApiRequest> pipeline processor, and another pipeline to execute a different custom operation, have a look at this post where I show how to publish Items using the Sitecore Item Web API.

For any custom pipeline you build for the Sitecore Item Web API, you must define a Parameter Object that inherits from Sitecore.ItemWebApi.Pipelines.OperationArgs:

using System.Collections.Generic;

using Sitecore.Data.Items;

using Sitecore.ItemWebApi.Pipelines;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Clone
{
    public class CloneArgs : OperationArgs
    {
        public CloneArgs(Item[] scope)
            : base(scope)
        {
        }

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

        public bool IsRecursive { get; set; }

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

I added three properties to the class above: a property to hold parent destinations for clones; another indicating whether all descendants should be cloned; and a property to hold a collection of the clones.

I then created a base class for processors of my custom pipeline for cloning:

using Sitecore.ItemWebApi.Pipelines;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Clone
{
    public abstract class CloneProcessor : OperationProcessor<CloneArgs>
    {
        protected CloneProcessor()
        {
        }
    }
}

The above class inherits from Sitecore.ItemWebApi.Pipelines.OperationProcessor which is the base class for most Sitecore Item Web API pipelines.

The following class serves as one processor of my custom cloning pipeline:

using System.Collections.Generic;

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

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Clone
{
    public class CloneItems : CloneProcessor
    {
        public override void Process(CloneArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Scope, "args.Scope");
            Assert.ArgumentNotNull(args.Destinations, "args.Destinations");
            IList<Item> clones = new List<Item>();
            foreach (Item itemToClone in args.Scope)
            {
                foreach (Item destination in args.Destinations)
                {
                    clones.Add(CloneItem(itemToClone, destination, args.IsRecursive));
                }   
            }

            args.Clones = clones;
        }

        private Item CloneItem(Item item, Item destination, bool isRecursive)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNull(destination, "destination");
            return item.CloneTo(destination, isRecursive);
        }
    }
}

The class above iterates over all Items in scope — these are the Items being cloned — and clones all to the specified destinations (parent Items of the clones).

I then spun up the following class to serve as another processor in my custom cloning pipeline:

using System.Linq;

using Sitecore.Diagnostics;
using Sitecore.Pipelines;

using Sitecore.ItemWebApi.Pipelines.Read;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Clone
{
    public class SetResult : CloneProcessor
    {
        public override void Process(CloneArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Clones, "args.Clones");
            if (args.Result == null)
            {
                ReadArgs readArgs = new ReadArgs(args.Clones.ToArray());
                CorePipeline.Run("itemWebApiRead", readArgs);
                args.Result = readArgs.Result;
            }
        }
    }
}

The above class delegates to the <itemWebApiRead> pipeline which retrieves the clones from Sitecore, and stores these in the Parameter Object instance for the custom cloning pipeline.

In order to handle custom requests in the Sitecore Item Web API, you must create a custom <itemWebApiRequest> pipeline processor. I put together the following class to handle my cloning operation:

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

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Request;
using Sitecore.Pipelines;
using Sitecore.Text;
using Sitecore.Web;

using Sitecore.Sandbox.ItemWebApi.Pipelines.Clone;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request
{
    public class ResolveCloneAction : RequestProcessor
    {
        public override void Process(RequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(RequestMethod, "RequestMethod");
            Assert.ArgumentNotNullOrEmpty(MultipleItemsDelimiter, "MultipleItemsDelimiter");
            if (!ShouldProcessRequest(args))
            {
                return;
            }

            IEnumerable<Item> destinations = GetDestinationItems();
            if (!destinations.Any())
            {
                Logger.Warn("Cannot process clone action: there are no destination items!");
                return;
            }
            
            CloneArgs cloneArgs = new CloneArgs(args.Scope) 
            { 
                Destinations = destinations,
                IsRecursive = DoRecursiveCloning() 
            };
            CorePipeline.Run("itemWebApiClone", cloneArgs);
            args.Result = cloneArgs.Result;
        }

        private bool ShouldProcessRequest(RequestArgs args)
        {
            // Is this the request method we care about?
            if (!AreEqualIgnoreCase(args.Context.HttpContext.Request.HttpMethod, RequestMethod))
            {
                return false;
            }

            // are multiple axes supplied?
            if (WebUtil.GetQueryString("scope").Contains(MultipleItemsDelimiter))
            {
                Logger.Warn("Cannot process clone action: multiple axes detected!");
                return false;
            }

            // are there any items in scope?
            if (!args.Scope.Any())
            {
                Logger.Warn("Cannot process clone action: there are no items in Scope!");
                return false;
            }

            return true;
        }

        private static bool AreEqualIgnoreCase(string one, string two)
        {
            return string.Equals(one, two, StringComparison.CurrentCultureIgnoreCase);
        }
        
        private IEnumerable<Item> GetDestinationItems()
        {
            char delimiter;
            Assert.ArgumentCondition(char.TryParse(MultipleItemsDelimiter, out delimiter), "MultipleItemsDelimiter", "MultipleItemsDelimiter must be a single character!");
            ListString destinations = new ListString(WebUtil.GetQueryString("destinations"), delimiter);
            return (from destination in destinations
                    let destinationItem = GetItem(destination)
                    where destinationItem != null
                    select destinationItem).ToList();
        }

        private Item GetItem(string path)
        {
            try
            {
                return Sitecore.ItemWebApi.Context.Current.Database.Items[path];
            }
            catch (Exception ex)
            {
                Logger.Error(ex);
            }

            return null;
        }

        private bool DoRecursiveCloning()
        {
            bool recursive;
            if (bool.TryParse(WebUtil.GetQueryString("recursive"), out recursive))
            {
                return recursive;
            }
            
            return false;
        }

        private string RequestMethod { get; set; }

        private string MultipleItemsDelimiter { get; set; }
    }
}

The above class ascertains whether it should handle the request: is the RequestMethod passed via configuration equal to the request method detected, and are there any Items in scope? I also built this processor to handle only one axe in order to keep the code simple.

Once the class determines it should handle the request, it grabs all destination Items from the context database — this is Sitecore.ItemWebApi.Context.Current.Database which is populated via the sc_database query string parameter passed via the request.

Further, the class above detects whether the cloning operation is recursive: should we clone all descendants of the Items in scope? This is also passed by a query string parameter.

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

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <itemWebApiClone>
        <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Clone.CloneItems, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Clone.SetResult, Sitecore.Sandbox" />
      </itemWebApiClone>
      <itemWebApiRequest>
        <processor patch:before="*[@type='Sitecore.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.ItemWebApi']"
                   type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveCloneAction, Sitecore.Sandbox">
          <RequestMethod>clone</RequestMethod>
          <MultipleItemsDelimiter>|</MultipleItemsDelimiter>
        </processor>
      </itemWebApiRequest>
    </pipelines>
  </sitecore>
</configuration>

Let’s clone the following Sitecore Item with descendants to two folders:

item-to-clone-destinations

In order to make this happen, I spun up the following HTML page using jQuery — no doubt the front-end gurus reading this are cringing when seeing the following code, but I am not much of a front-end developer:

<!DOCTYPE html>
<html lang="en">
	<head>
		<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
		<script src="//cdnjs.cloudflare.com/ajax/libs/json3/3.3.2/json3.min.js"></script>
		<script src="//cdnjs.cloudflare.com/ajax/libs/prettify/r224/prettify.js"></script>
		<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/prettify/r224/prettify.css" />
	</head>
	<body>
		<img width="400" style="display: block; margin-left: auto; margin-right: auto" src="/assets/img/clone-all-the-things.jpg" />
		<input type="button" id="button" value="Clone" style="width:100px;height:50px;font-size: 24px;" />
		<h2 id="confirmation" style="display: none;">Whoa! Something happened!</h2>
		<div id="working" style="display: none;"><img style="display: block; margin-left: auto; margin-right: auto" src="/assets/img/arrow-working.gif" /></div>
		<pre id="responseContainer" class="prettyprint" style="display: none;"><code id="response" class="language-javascript"></code></pre>
		<script type="text/javascript">
		$('#button').click(function() {
			$('#confirmation').hide();
			$('#responseContainer').hide();
			$('#working').show();
			$.ajax({
					type:'clone',
					url: "http://sandbox7/-/item/v1/sitecore/content/Home/Landing Page One?scope=s&destinations=/sitecore/content/Home/Clones|/sitecore/content/Home/Some More Clones&recursive=true&sc_database=master",
					headers:{
						"X-Scitemwebapi-Username":"extranet\\ItemWebAPI",
						"X-Scitemwebapi-Password":"1t3mW3bAP1"}
				}).done(function(response) {
					$('#confirmation').show();
					$('#response').html(JSON.stringify(response, null, 4));
					$('#working').hide();
					$('#responseContainer').show();
				});
		});
		</script>
	</body>
</html>

Plus, please pardon the hard-coded Sitecore credentials — I know you would never store a username and password in front-end code, right? ;)

The above HTML page looks like this on initial load:

clone-items-html-page-no-data

I then clicked the ‘Clone’ button, and saw the following:

cloned-items-html-page

As you can see, the target Item with descendants were cloned to the destination folders set in the jQuery above:

items-cloned-sitecore

If you have any thoughts on this, or have other ideas around customizing the Sitecore Item Web API, please share in a comment.


Viewing all 112 articles
Browse latest View live