Home » Json

Category Archives: Json

Advertisements

Download Random Giphy Images and Save to the Media Library Via a Custom Content Editor Image Field in Sitecore

In my previous post I created a custom Content Editor image field in the Sitecore Experience Platform. This custom image field gives content authors the ability to download an image from outside of their Sitecore instance; save the image to the Media Library; and then map that resulting Media Library Item to the custom Image field on an Item in the content tree.

Building that solution was a great way to spend a Friday night (and even the following Saturday morning) — though I bet some people would argue watching cat videos on YouTube might be better way to spend a Friday night — and even gave me the opportunity to share that solution with you guys.

After sharing this post on Twitter, Sitecore MVP Kam Figy replied to that tweet with the following:

This gave me an idea: why not modify the solution from my previous post to give the ability to download a random image from Giphy via their the API?

You might be asking yourself “what is this Giphy thing?” Giphy is basically a site that allows users to upload images — more specifically animated GIFs — and then associate those uploaded images with tags. These tags are used for finding images on their site and also through their API.

You might be now asking “what’s the point of Giphy?” The point is to have fun and share a laugh; animated GIFs can be a great way of achieving these.

Some smart folks out there have built integrations into other software platforms which give users the ability pull images from the Giphy API. An example of this can be seen in Slack messaging application.

As a side note, if you aren’t on the Sitecore Community Slack, you probably should be. This is the fastest way to get help, share ideas and even have some good laughs from close to 1000 Sitecore developers, architects and marketers from around the world in real-time. If you would like to join the Sitecore Community Slack, please let me know and I will send you an invite though please don’t ask for an invite in comments section below on this post. Instead reach out to me on Twitter: @mike_i_reynolds. You can also reach out to Sitecore MVP Akshay Sura: @akshaysura13.

Here’s an example of me calling up an image using some tags in one of the channels on the Sitecore Community Slack using the Giphy integration for Slack:

giphy-image-slack

There really isn’t anything magical about the Giphy API — all you have to do is send an HTTP request with some query string parameters. Giphy’s API will then give you a response in JSON:

giphy-image-json

Before I dig into the solution below, I do want to let you know I will not be talking about all of the code in the solution. Most of the code was repurposed from my previous post. If you have not read my previous post, please read it before moving forward so you have a full understanding of how this works.

Moreover, do note there is probably no business value in using the following solution as is — it was built for fun on another Friday night and Saturday morning. 😉

To get data out of this JSON response, I decided to use Newtonsoft.Json. Why did I choose this? It was an easy decision: Newtonsoft.Json comes with Sitecore “out of the box” so it was convenient for me to choose this as a way to parse the JSON coming from the Giphy API.

I created the following model classes with JSON to C# property mappings:

using Newtonsoft.Json;

namespace Sitecore.Sandbox.Providers
{
    public class GiphyData
    {
        [JsonProperty("type")]
        public string Type { get; set; }

        [JsonProperty("id")]
        public string Id { get; set; }

        [JsonProperty("url")]
        public string Url { get; set; }

        [JsonProperty("image_original_url")]
        public string ImageOriginalUrl { get; set; }

        [JsonProperty("image_url")]
        public string ImageUrl { get; set; }

        [JsonProperty("image_mp4_url")]
        public string ImageMp4Url { get; set; }

        [JsonProperty("image_frames")]
        public string ImageFrames { get; set; }

        [JsonProperty("image_width")]
        public string ImageWidth { get; set; }

        [JsonProperty("image_height")]
        public string ImageHeight { get; set; }

        [JsonProperty("fixed_height_downsampled_url")]
        public string FixedHeightDownsampledUrl { get; set; }

        [JsonProperty("fixed_height_downsampled_width")]
        public string FixedHeightDownsampledWidth { get; set; }

        [JsonProperty("fixed_height_downsampled_height")]
        public string FixedHeightDownsampledHeight { get; set; }

        [JsonProperty("fixed_width_downsampled_url")]
        public string FixedWidthDownsampledUrl { get; set; }

        [JsonProperty("fixed_width_downsampled_width")]
        public string FixedWidthDownsampledWidth { get; set; }

        [JsonProperty("fixed_width_downsampled_height")]
        public string FixedWidthDownsampledHeight { get; set; }

        [JsonProperty("fixed_height_small_url")]
        public string FixedHeightSmallUrl { get; set; }

        [JsonProperty("fixed_height_small_still_url")]
        public string FixedHeightSmallStillUrl { get; set; }

        [JsonProperty("fixed_height_small_width")]
        public string FixedHeightSmallWidth { get; set; }

        [JsonProperty("fixed_height_small_height")]
        public string FixedHeightSmallHeight { get; set; }

        [JsonProperty("fixed_width_small_url")]
        public string FixedWidthSmallUrl { get; set; }

        [JsonProperty("fixed_width_small_still_url")]
        public string FixedWidthSmallStillUrl { get; set; }

        [JsonProperty("fixed_width_small_width")]
        public string FixedWidthSmallWidth { get; set; }

        [JsonProperty("fixed_width_small_height")]
        public string FixedWidthSmallHeight { get; set; }

        [JsonProperty("username")]
        public string Username { get; set; }

        [JsonProperty("caption")]
        public string Caption { get; set; }
    }
}
using Newtonsoft.Json;

namespace Sitecore.Sandbox.Providers
{
    public class GiphyMeta
    {
        [JsonProperty("status")]
        public int Status { get; set; }

        [JsonProperty("msg")]
        public string Message { get; set; }
    }
}
using Newtonsoft.Json;

namespace Sitecore.Sandbox.Providers
{
    public class GiphyResponse
    {
        [JsonProperty("data")]
        public GiphyData Data { get; set; }

        [JsonProperty("meta")]
        public GiphyMeta Meta { get; set; }
    }
}

Every property above in every class represents a JSON property/object in the response coming back from the Giphy API.

Now, we need a way to make a request to the Giphy API. I built the following interface whose instances will do just that:

namespace Sitecore.Sandbox.Providers
{
    public interface IGiphyImageProvider
    {
        GiphyData GetRandomGigphyImageData(string tags);
    }
}

The following class implements the interface above:

using System;
using System.Net;

using Sitecore.Diagnostics;

using Newtonsoft.Json;
using System.IO;

namespace Sitecore.Sandbox.Providers
{
    public class GiphyImageProvider : IGiphyImageProvider
    {
        private string RequestUrlFormat { get; set; }

        private string ApiKey { get; set; }

        public GiphyData GetRandomGigphyImageData(string tags)
        {
            Assert.IsNotNullOrEmpty(RequestUrlFormat, "RequestUrlFormat");
            Assert.IsNotNullOrEmpty(ApiKey, "ApiKey");
            Assert.ArgumentNotNullOrEmpty(tags, "tags");
            string response = GetJsonResponse(GetRequestUrl(tags));
            if(string.IsNullOrWhiteSpace(response))
            {
                return new GiphyData();
            }

            try
            {
                GiphyResponse giphyResponse = JsonConvert.DeserializeObject<GiphyResponse>(response);
                if(giphyResponse != null && giphyResponse.Meta != null && giphyResponse.Meta.Status == 200 && giphyResponse.Data != null)
                {
                    return giphyResponse.Data;
                }
            }
            catch(Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return new GiphyData();
        }

        protected virtual string GetRequestUrl(string tags)
        {
            Assert.ArgumentNotNullOrEmpty(tags, "tags");
            return string.Format(RequestUrlFormat, ApiKey, Uri.EscapeDataString(tags));
        }

        protected virtual string GetJsonResponse(string requestUrl)
        {
            Assert.ArgumentNotNullOrEmpty(requestUrl, "requestUrl");
            try
            {
                WebRequest request = HttpWebRequest.Create(requestUrl);
                request.Method = "GET";
                string json;
                using (WebResponse response = request.GetResponse())
                {
                    using (Stream responseStream = response.GetResponseStream())
                    {
                        using (StreamReader sr = new StreamReader(responseStream))
                        {
                            return sr.ReadToEnd();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return string.Empty;
        }
    }
}

Code in the methods above basically take in tags for the type of random image we want from Giphy; build up the request URL — the template of the request URL and API key (I’m using the public key which is open for developers to experiment with) are populated via the Sitecore Configuration Factory (have a look at the patch include configuration file further down in this post to get an idea of how the properties of this class are populated); make the request to the Giphy API; get back the response; hand the response over to some Newtonsoft.Json API code to parse JSON into model instances of the classes shown further above in this post; and then return the nested model instances.

I then created the following Sitecore.Shell.Applications.ContentEditor.Image subclass which represents the custom Content Editor Image field:

using System;

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

using Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary;
using Sitecore.Sandbox.Providers;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class GiphyImage : Sitecore.Shell.Applications.ContentEditor.Image
    {
        private IGiphyImageProvider GiphyImageProvider { get; set; }

        public GiphyImage()
            : base()
        {
            GiphyImageProvider = GetGiphyImageProvider();
        }

        protected virtual IGiphyImageProvider GetGiphyImageProvider()
        {
            IGiphyImageProvider giphyImageProvider = Factory.CreateObject("imageProviders/giphyImageProvider", false) as IGiphyImageProvider;
            Assert.IsNotNull(giphyImageProvider, "The giphyImageProvider was not properly defined in configuration");
            return giphyImageProvider;
        }

        public override void HandleMessage(Message message)
        {
            Assert.ArgumentNotNull(message, "message");
            if (string.Equals(message.Name, "contentimage:downloadGiphy", StringComparison.CurrentCultureIgnoreCase))
            {
                GetInputFromUser();
                return;
            }

            base.HandleMessage(message);
        }

        protected void GetInputFromUser()
        {
            RunProcessor("GetGiphyTags", new ClientPipelineArgs());
        }

        protected virtual void GetGiphyTags(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                SheerResponse.Input("Enter giphy tags:", string.Empty);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["tags"] = args.Result;
                args.IsPostBack = false;
                RunProcessor("GetGiphyImageUrl", args);
            }
            else
            {
                CancelOperation(args);
            }
        }

        protected virtual void GetGiphyImageUrl(ClientPipelineArgs args)
        {
            GiphyData giphyData = GiphyImageProvider.GetRandomGigphyImageData(args.Parameters["tags"]);
            if (giphyData == null || string.IsNullOrWhiteSpace(giphyData.ImageUrl))
            {
                SheerResponse.Alert("Unfortunately, no image matched the tags you specified. Please try again.");
                CancelOperation(args);
                return;
            }

            args.Parameters["imageUrl"] = giphyData.ImageUrl;
            args.IsPostBack = false;
            RunProcessor("ChooseMediaLibraryFolder", args);
        }

        protected virtual void RunProcessor(string processor, ClientPipelineArgs args)
        {
            Assert.ArgumentNotNullOrEmpty(processor, "processor");
            Sitecore.Context.ClientPage.Start(this, processor, args);
        }

        public void ChooseMediaLibraryFolder(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                Dialogs.BrowseItem
                (
                    "Select A Media Library Folder",
                    "Please select a media library folder to store the Giphy image.",
                    "Applications/32x32/folder_into.png",
                    "OK",
                    "/sitecore/media library", 
                    string.Empty
                );

                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                Item folder = Client.ContentDatabase.Items[args.Result];
                args.Parameters["mediaLibaryFolderPath"] = folder.Paths.FullPath;
                args.IsPostBack = false;
                RunProcessor("DownloadImage", args);
            }
            else
            {
                CancelOperation(args);
            }
        }

        protected virtual void DownloadImage(ClientPipelineArgs args)
        {
            DownloadImageToMediaLibraryArgs downloadArgs = new DownloadImageToMediaLibraryArgs
            {
                Database = Client.ContentDatabase,
                ImageUrl = args.Parameters["imageUrl"],
                MediaLibaryFolderPath = args.Parameters["mediaLibaryFolderPath"]
            };

            CorePipeline.Run("downloadImageToMediaLibrary", downloadArgs);
            SetMediaItemInField(downloadArgs);
        }

        protected virtual void SetMediaItemInField(DownloadImageToMediaLibraryArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(string.IsNullOrWhiteSpace(args.MediaId) || string.IsNullOrWhiteSpace(args.MediaPath))
            {
                return;
            }

            XmlValue.SetAttribute("mediaid", args.MediaId);
            Value = args.MediaPath;
            Update();
            SetModified();
        }

        protected virtual void CancelOperation(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.AbortPipeline();
        }
    }
}

The class above does not differ much from the Image class I shared in my previous post. The only differences are in the instantiation of an IGiphyImageProvider object using the Sitecore Configuration Factory — this object is used for getting the Giphy image URL from the Giphy API; the GetGiphyTags() method prompts the user for tags used in calling up a random image from Giphy; and in the GetGiphyImageUrl() method which uses the IGiphyImageProvider instance to get the image URL. The rest of the code in this class is unmodified from the Image class shared in my previous post.

I then defined the IGiphyImageProvider code in the following patch include configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <imageProviders>
      <giphyImageProvider type="Sitecore.Sandbox.Providers.GiphyImageProvider, Sitecore.Sandbox" singleInstance="true">
        <RequestUrlFormat>http://api.giphy.com/v1/gifs/random?api_key={0}&amp;tag={1}</RequestUrlFormat>
        <ApiKey>dc6zaTOxFJmzC</ApiKey>
      </giphyImageProvider>
    </imageProviders>
  </sitecore>
</configuration>

Be sure to check out the patch include configuration file from my previous post as it contains the custom pipeline that downloads images from a URL.

You should also refer my previous post which shows you how to register a custom Content Editor field in the core database of Sitecore.

Let’s test this out.

We need to add this new field to a template. I’ve added it to the “out of the box” Sample Item template:
giphy-image-sample-item-new-field

My Home item uses the above template. Let’s download a random Giphy image on it:

giphy-image-home-1

I then supplied some tags for getting a random image:

giphy-image-home-2

Let’s choose a place to save the image in the Media Library:

giphy-image-home-3

As you can see, the image was downloaded and saved into the Media Library in the selected folder, and then saved in the custom field on the Home item:

giphy-image-home-4

If you are curious, this is the image that was returned by the Giphy API:

giphy-image-downloaded

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

Advertisements

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

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.

Embedded Tweets in Sitecore: A Proof of Concept

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

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

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

tweet-url-wordpress

This is what is seen on the rendered page:

tweet-embedded

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

tweet-api-json

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

using System.Runtime.Serialization;

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

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

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

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

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

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

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

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

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

        private string TwitterApiUrlFormat { get; set; }

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

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

                return _HtmlCache;
            }
        }

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

            args.Result.FirstPart = ExpandTweetUrls(args.Result.FirstPart);
        }

        private static bool ShouldFieldBeProcessed(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.FieldTypeKey, "args.FieldTypeKey");
            string fieldTypeKey = args.FieldTypeKey.ToLower();
            return fieldTypeKey == "text"
                    || fieldTypeKey == "rich text"
                    || fieldTypeKey == "single-line text"
                    || fieldTypeKey == "multi-line text";
        }

        private void AssertRequired()
        {
            Assert.IsNotNullOrEmpty(TwitterWidgetScriptTag, "TwitterWidgetScriptTag must be set! Check your configuration!");
            Assert.IsNotNullOrEmpty(TwitterApiUrlFormat, "TwitterApiUrlFormat must be set! Check your configuration!");
            Assert.IsNotNullOrEmpty(TweetPattern, "TweetPattern must be set! Check your configuration!");
        }

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

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

            return htmlExpanded;
        }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Let’s see this in action!

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

tweets-rich-text

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

tweets-rich-text-html

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

tweets-front-end

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

Add Additional Item Properties in Sitecore Item Web API Responses

The other day I was exploring pipelines of the Sitecore Item Web API, and took note of the itemWebApiGetProperties pipeline. This pipeline adds information about an item in the response returned by the Sitecore Item Web API. You can find this pipeline at /configuration/sitecore/pipelines/itemWebApiGetProperties in \App_Config\Include\Sitecore.ItemWebApi.config.

The following properties are set for an item in the response via the lonely pipeline processor — /configuration/sitecore/pipelines/itemWebApiGetProperties/processor[@type=”Sitecore.ItemWebApi.Pipelines.GetProperties.GetProperties, Sitecore.ItemWebApi”] — that ships with the Sitecore Item Web API:

out-of-the-box-properties

Here’s an example of what the properties set by the above pipeline processor look like in the response — I invoked a read request to the Sitecore Item Web API via a copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK:

properties-out-of-box-console

You might be asking “how difficult would it be to add in my own properties?” It’s not difficult at all!

I whipped up the following itemWebApiGetProperties pipeline processor to show how one can add more properties for an item:

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

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.GetProperties
{
    public class GetEvenMoreProperties : GetPropertiesProcessor
    {
        public override void Process(GetPropertiesArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            arguments.Properties.Add("ParentID", arguments.Item.ParentID.ToString());
            arguments.Properties.Add("ChildrenCount", arguments.Item.Children.Count);
            arguments.Properties.Add("Level", arguments.Item.Axes.Level);
            arguments.Properties.Add("IsItemClone", arguments.Item.IsItemClone);
            arguments.Properties.Add("CreatedBy", arguments.Item["__Created by"]);
            arguments.Properties.Add("UpdatedBy", GetItemUpdatedBy(arguments.Item));
        }

        private static Dynamic GetItemUpdatedBy(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            string[] usernamePieces = item["__Updated by"].Split('\\');

            Dynamic username = new Dynamic();
            if (usernamePieces.Length > 1)
            {
                username["Domain"] = usernamePieces[0];
                username["Username"] = usernamePieces[1];
            }
            else if (usernamePieces.Length > 0)
            {
                username["Username"] = usernamePieces[0];
            }

            return username;
        }
    }
}

The ParentID, ChildrenCount, Level and IsItemClone properties are simply added to the properties SortedDictionary within the GetPropertiesArgs instance, and will be serialized as is.

For the UpdatedBy property, I decided to leverage the Sitecore.ItemWebApi.Dynamic class in order to have the username set in the “__Updated by” field be represented by a JSON object. This JSON object sets the domain and username — without the domain — into different JSON properties.

As a side note — when writing your own service code for the Sitecore Item Web API — I strongly recommend using instances of the Sitecore.ItemWebApi.Dynamic class — or something similar — for complex objects. Developers writing code to consume your JSON will thank you many times for it. 🙂

I registered my new processor to the itemWebApiGetProperties pipeline in my Sitecore instance’s \App_Config\Include\Sitecore.ItemWebApi.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- there's stuff here -->
      <itemWebApiGetProperties>
        <processor type="Sitecore.ItemWebApi.Pipelines.GetProperties.GetProperties, Sitecore.ItemWebApi" />
        <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.GetProperties.GetEvenMoreProperties, Sitecore.Sandbox" />
      </itemWebApiGetProperties>
      <!-- there's stuff here as well -->
  </sitecore>
</configuration>

Let’s take this for a spin.

I ran the console application again to see what the response now looks like:

additional-properties

As you can see, our additional properties are now included in the response.

If you can think of other item properties that would be useful for Sitecore Item Web API client applications, please share in a comment.

Until next time, have a Sitecorelicious day!

Copy Sitecore Item Field Values To Your Clipboard as JSON

With all the exciting things happening around json recently — the Sitecore Item Web API (check out this awesome presentation on the Sitecore Item Web API) and pasting JSON As classes in ASP.NET and Web Tools 2012.2 RC — I’ve been plagued by json on the brain.

For the past few weeks, I’ve been thinking about building a Sitecore client command that copies fields values from a Sitecore item into a string of json via the clipboard, though have been struggling with whether such a command has any utility.

Tonight, I decided to build such a command. Here is what I came up with.

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

using Sitecore.Configuration;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Data.Templates;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Utilities.Serialization.Base;
using Sitecore.Sandbox.Utilities.Serialization;

namespace Sitecore.Sandbox.Commands
{
    public class CopyItemAsJsonToClipboard : ClipboardCommand
    {
        private const bool AlertIfClipboardCopyNotSupported = true;
        private static readonly CopyToClipboard CopyToClipboard = new CopyToClipboard();

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

                return _StandardTemplate;
            }
        }

        private static Template GetStandardTemplate()
        {
            return TemplateManager.GetTemplate(Settings.DefaultBaseTemplate, Context.ContentDatabase);
        }
        
        public override void Execute(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");

            if (IsSupported(AlertIfClipboardCopyNotSupported))
            {
                IEnumerable<Field> fields = GetNonStandardTemplateFields(commandContext);
                CopyToClipBoard(GetFieldsAsJson(fields));
            }
        }

        private static string GetFieldsAsJson(IEnumerable<Field> fields)
        {
            Assert.ArgumentNotNull(fields, "fields");
            IList<string> list = new List<string>();
            foreach(Field field in fields)
            {
                list.Add(string.Format("\"{0}\":\"{1}\"", field.Name, EscapeNewlines(field.Value)));
            }

            return string.Concat("{", string.Join(",", list), "}");
        }

        private static string EscapeNewlines(string value)
        {
            Assert.ArgumentNotNull(value, "value");
            return ReplaceSubstrings
            (
                value,
                new KeyValuePair<string, string>[] 
                { 
                    new KeyValuePair<string, string>("\n", "\\n"),
                    new KeyValuePair<string, string>("\r", "\\r")
                }
            );
        }

        private static string ReplaceSubstrings(string source, IEnumerable<KeyValuePair<string, string>> oldNewValues)
        {
            Assert.ArgumentNotNull(source, "source");
            Assert.ArgumentNotNull(oldNewValues, "oldNewValues");
            foreach (KeyValuePair<string, string> oldNewValue in oldNewValues)
            {
                source = source.Replace(oldNewValue.Key, oldNewValue.Value);
            }

            return source;
        }

        private IEnumerable<Field> GetNonStandardTemplateFields(CommandContext commandContext)
        {
            return GetNonStandardTemplateFields(GetItemWithAllFields(commandContext));
        }

        private IEnumerable<Field> GetNonStandardTemplateFields(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return GetNonStandardTemplateFields(item.Fields);
        }

        private IEnumerable<Field> GetNonStandardTemplateFields(IEnumerable<Field> fields)
        {
            Assert.ArgumentNotNull(fields, "fields");
            return fields.Where(field => !IsStandardTemplateField(field));
        }

        private static void CopyToClipBoard(string json)
        {
            if(!string.IsNullOrEmpty(json))
            {
                SheerResponse.Eval(GetCopyToClipBoardJavascript(json));
            }
        }

        private static string GetCopyToClipBoardJavascript(string json)
        {
            Assert.ArgumentNotNullOrEmpty(json, "json");
            return string.Format("window.clipboardData.setData('Text', '{0}')", json);
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            Item item = GetItemWithAllFields(commandContext);
            bool makeEnabled = CopyToClipboard.QueryState(commandContext) == CommandState.Enabled && HasFields(item);

            if (makeEnabled)
            {
                return CommandState.Enabled;
            }

            return CommandState.Disabled;
        }

        private static bool HasFields(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item.Fields.Any();
        }

        private static Item GetItemWithAllFields(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            Item item = GetItem(commandContext);

            if (item != null)
            {
                item.Fields.ReadAll();
            }
            
            return item;
        }

        public bool IsStandardTemplateField(Field field)
        {
            Assert.ArgumentNotNull(StandardTemplate, "StandardTemplate");
            return StandardTemplate.ContainsField(field.ID);
        }

        private static Item GetItem(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            Assert.ArgumentNotNull(commandContext.Items, "commandContext.Items");
            Assert.ArgumentCondition(commandContext.Items.Any(), "commandContext.Items", "There must be at least one item in the array!");
            return commandContext.Items.FirstOrDefault();
        }
    }
}

The command above iterates over all fields on the selected item — not including those that are defined in the Standard Template — and builds a string of json containing field names accompanied by their values.

I found this article by John West to be extremely helpful in writing the code above to determine if a field belongs to the Standard Template — this is used when leaving out these fields in our resulting json string.

Further, the command is only enabled when the selected item has fields coupled with the CommandState returned by a call to the QueryState() method on an instance of Sitecore.Shell.Framework.Commands.CopyToClipboard — this object’s QueryState() method checks things we care about, and I wanted to leverage its logic.

I then mapped the command above in a patch include file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <sitecore>
    <commands>
      <command name="item:copyitemasjsontoclipboard" type="Sitecore.Sandbox.Commands.CopyItemAsJsonToClipboard,Sitecore.Sandbox"/>
    </commands>
  </sitecore>
</configuration>

Now that we have our command, let’s create an item context menu option for it:

copy-as-json-context-menu

Let’s see this thing in action.

I created an item for testing:

jsonify-me-item

I right-clicked on our new item to launch its context menu, and then clicked on the ‘Copy As Json’ menu option:

copy-as-json-context-menu-jsonify

I pasted the following from my clipboard into Notepad++:

json-string

If you can think how this, or something similar could be useful, please drop a comment.