Fix invalid dynamic placeholders after upgrading to Sitecore 9

Sitecore 9 supports Dynamic Placeholders OOTB and does no longer require the popular Dynamic Placeholder third party module. The format used by Sitecore for the dynamic placeholders is different than the format used by the Dynamic Placeholder module:

Old dynamic placeholder pattern: placeholderName_renderingId. Example:

content_acda6718-0907-4a6c-8c6b-b157f5d708ac

Sitecore placeholder pattern: {placeholder key}-{rendering unique suffix}-{unique suffix within rendering}. Example:

content-{acda6718-0907-4a6c-8c6b-b157f5d708ac}-0

You will have to update your presentation details for these differences otherwise the renderings will not be shown on the page.

Richard Seal already wrote a great blog post about how to upgrade this using SPE. This works well and also does a great job of explaining other tasks that need to be performed to move to Sitecore’s dynamic placeholders. However it doesn’t cover below 2 scenario’s:

  • Nested Dynamic placeholder: a rendering with a dynamic placeholder can contain another rendering with a dynamic placeholder.
  • Multilingual: a item can have dynamic placeholders in multiple language versions of the final layout.

Approach

I started with Martin Miles’ code which iterates through all the renderings and updates placeholders. From here the logic is updated to loop through each language version of every item. Below code updates every match of an old style dynamic placeholder so nested placeholders are taken care of appropriately:

bool requiresUpdate = false;

foreach (RenderingDefinition rendering in device.Renderings)
{
    if (!string.IsNullOrWhiteSpace(rendering.Placeholder))
    {
        var newPlaceholder = rendering.Placeholder;
        foreach (Match match in Regex.Matches(newPlaceholder, PlaceholderRegex, RegexOptions.IgnoreCase))
        {
            var renderingId = match.Value;
            var newRenderingId = "-{" + renderingId.ToUpper().Substring(1) + "}-0";

            newPlaceholder = newPlaceholder.Replace(match.Value, newRenderingId);

            requiresUpdate = true;
        }

        result.Add(new KeyValuePair<string, string>(rendering.Placeholder, newPlaceholder));
        rendering.Placeholder = newPlaceholder;
    }
}

if (requiresUpdate)
{
    string newXml = details.ToXml();

    using (new EditContext(currentItem))
    {
        LayoutField.SetFieldValue(field, newXml);
    }
}

Working Solution

The final working solution is uploaded to my Github and can be found here A few notes on solution:

  • Admin page is added at <sitecoreurl>/sitecore/admin/custom/migratedp.aspx
  • All content under /sitecore/content is updated, template standard values are not
  • Make sure to back up content before running this

 

Advertisements

Habitat Demo Group and Visual Studio 2017

Recently I had the privilege of attending a Sitecore Helix training. I’ve been using Visual Studio 2017 for a while now and had no trouble getting the Habitat solution running, all I had to do was update the buildToolsVersion to 15.0 in the gulp-config.js file and everything worked as expected.

Getting the Demo.Group solution running was a little bit harder. This solution is necessary for these exercises. There is no buildToolsVersion property in the gulp-config.js, instead version 14.0 is hardcoded in the gulpfile.js. I added the property in the gulp-config.js and used this property in the gulpfile.js instead of 14.0.

After this change it got a little further but still was not able to successfully run all the Gulp tasks. This time it gave me this error: “throw new PluginError(constants.PLUGIN_NAME, ‘No MSBuild Version was supplied!’); ” This stackexchange post has the answer to this issue. The Sitecore.Demo.Group solution uses an old version of gulp-msbuild, updating this to version 0.4.4 in the package.json resolved all remaining issues.

 

Sitecore use OAuth2 login with OWIN

As an experiment I wanted to see if it would be possible to use the social logins such as google with Sitecore using a similar approach as a plain MVC app. More details around doing this without Sitecore can be found here Notice that the code used in ASP .NET MVC relies on OWIN authentication middleware and ASP .NET Identity. This makes sense in ASP .NET, but we’ll have to reconsider this when integrating with Sitecore. A similar post using OWIN in Sitecore using Federated Authentication can be found here.

Sitecore and OWIN

Using OWIN with Sitecore makes sense, it is certainly possible to not use OWIN and rely on a custom implementation instead. There are some blog posts as well that will help you get started.

Given the security implications of getting a custom implementation slightly incorrect, it is highly recommended to use a proven solution like the OWIN middleware.

Additionally OWIN is widely used in regular ASP .NET applications. OWIN comes as a nuget package which makes it straightforward to update and take advantage of new features.

Sitecore and ASP .NET Identity

Using Sitecore with ASP .NET Identity does not make sense to me as Sitecore still uses ASP .NET membership. Using ASP .NET Identity would add another component that I could not see a use or benefit for, but please let me know in case anyone does. Some code in this post is from this post which describes how to use OWIN without ASP .NET Identity in ASP .NET MVC.

Configuring OWIN Middleware

Below code configures the OWIN middleware to use google authentication:

using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Google;
using Owin;

namespace SitecoreOwinGoogle
{
    public partial class Startup
    {
        private void ConfigureAuth(IAppBuilder app)
        {
            var cookieOptions = new CookieAuthenticationOptions
            {
                LoginPath = new PathString("/Login")
            };

            app.UseCookieAuthentication(cookieOptions);

            app.SetDefaultSignInAsAuthenticationType(cookieOptions.AuthenticationType);

            app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions
            {
                ClientId = "your client id",
                ClientSecret = "your client secret"
            });
        }
    }
}

Notice in the usings that only OWIN is referenced, there are no ASP .NET Identity referenes added. ClientId and ClientSecret can be obtained from from Google Developers Console Never store passwords and other sensitive data in source code, for best practices see here

Login Through Google

In the OWIN middleware /Login is set as the login path. This can be set to a different path as well as long as there is code running when that path is hit to handle the login. I added a controller rendering here to transfer control to google:

public ActionResult Login(string returnUrl)
{
    return new ChallengeResult("Google",
      string.Format("/Login/ExternalLoginCallback?ReturnUrl={0}", returnUrl));
}

The ExternalLoginCallback will redirect the user to secure page that he was trying to navigate to before the OWIN middleware kicked in. It is important to run this code in the path specified in the redirectUri parameter on the ChallengeResult constructor, in this case “/Login/ExternalLoginCallback”.  Again I’m using a controller rendering for this.

public ActionResult ExternalLoginCallback(string returnUrl)
{
    return new RedirectResult(returnUrl);
}

I have used the same ChallengeResult class as ASP .NET MVC:

// Used for XSRF protection when adding external logins
private const string XsrfKey = "XsrfId";

internal class ChallengeResult : HttpUnauthorizedResult
{
    public ChallengeResult(string provider, string redirectUri)
        : this(provider, redirectUri, null)
    {
    }

    public ChallengeResult(string provider, string redirectUri, string userId)
    {
        LoginProvider = provider;
        RedirectUri = redirectUri;
        UserId = userId;
    }

    public string LoginProvider { get; set; }
    public string RedirectUri { get; set; }
    public string UserId { get; set; }

    public override void ExecuteResult(ControllerContext context)
    {
        var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
        if (UserId != null)
        {
            properties.Dictionary[XsrfKey] = UserId;
        }
        context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
    }
}

Redirect back to Sitecore

After google completed validating the user it will redirect back to Sitecore. By default OWIN expects this at /signin-google. Here OWIN will set the cookie used for the CookieAuthentication. Make sure to configure this page as an authorized redirect URI in google’s console.

Sitecore will try to handle the request which is made to /signin-google, Sitecore should not touch this request and let OWIN handle it. The easiest way to achieve this is by adding this page to the IgnoreUrlPrefixes setting. From there OWIN will call the callback URL specified in the ChallengeResult and this will redirect back to page that is secured.

Securing pages

That is all for the OWIN and OAuth code. Now that this is in place a controller action method can be decorated with the Authorize attribute to trigger the authentication flow. Below diagram shows the end-to-end flow:

Authentication flow

Further considerations

As mentioned before this blog post is a start in using OWIN with OAuth2 in Sitecore. However there are many other items to explore:

  • Multisite support: perhaps you have a multisite solution and some sites need social logins and other sites need a different login method. OWIN supports branching the pipeline to allow for different configurations based on a condition e.g. hostname
  • Support different social provider: this blog post only works with google, but OWIN supports several different logins like Facebook, Twitter, Linkedin or Microsoft.
  • Logoff: this post only covers login, but it should be fairly straightforward to implement logoff in a similar fashion.
  • Sitecore Virtual Users: the authentication in this post is basic, either you are successfully logged in from google or you are not. Most real world applications are more complicated and different users have different permissions. In these cases it can be helpful to create a Sitecore virtual user and assign Sitecore roles.
  • OAuth2 scope: there is a scope parameter which supports retrieving additional information about the user from google. The users will have to accept this first on the consent screen when they use google’s system to log in.

Summary

This post showed how to set up OWIN with Sitecore and OAuth2. Using OWIN significantly simplified working with OAuth2 and google. There are a number of additional considerations that must be taken into account before using this in a production scenario.

Sitecore namespace issues

Since I ran into this silly issue a couple of times I decided to dig into it a little bit and write a post about it. As developers we like to structure our solutions and projects by applying consistent naming conventions. Most Sitecore projects use a naming convention like this prefix.Sitecore.description. Usually the prefix is something like a company name or a brand name, and the description is something that describes the project’s role in the solution. There can also be multiple prefixes and descriptions separated by a period. Looking at Habitat an example of description could be Feature.Accounts.

A popular approach is to keep project names, assembly names, folder structure and namespaces all in sync for clarity. If you go with this approach you will end up with a namespace like MyBrand.Sitecore.Features.Accounts. This is where things can get a little messy because “Sitecore” is in the namespace and this can conflict with references to Sitecore DLL’s. Consider below piece of code:

namespace MyBrand.Sitecore.Features.Accounts
{
    using Sitecore;

    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = Sitecore.Context.Item;

            return item.Name;
        }
    }
}

This code will create below compile error:

Error CS0234 The type or namespace name ‘Context’ does not exist in the namespace ‘MyBrand.Sitecore’ (are you missing an assembly reference?)

Problem

The problem here is how namespace resolution works for nested namespaces, more details can be found in the C# language specification here. As the compile error suggests it is looking for the Context object in our class instead of in Sitecore in the root scope. The good news is that there are at least 3 different ways to solve this:

1. Avoid using anything before “Sitecore” in namespace

The same code will compile just fine when “Sitecore” is the first part of the namespace, see line 1. This is the easiest solution if changing the namespace is not a problem.

namespace Sitecore.Features.Accounts
{
    using Sitecore;

    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = Sitecore.Context.Item;

            return item.Name;
        }
    }
}

2. Use namespace alias

This is my least favorite way to solve this but it also seems to be the most common solution. The Sitecore namespace from the Sitecore DLL can be declared as an alias and then used to reference:

using RealSitecore = Sitecore;

namespace MyBrand.Sitecore.Features.Accounts
{
    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = RealSitecore.Context.Item;

            return item.Name;
        }
    }
}

3. Use global namespace alias

Instead of making up a random name for Sitecore as in the last solution, it feels a lot cleaner to use the global namespace alias to tell the compiler to look in the global namespace instead:

namespace MyBrand.Sitecore.Features.Accounts
{
    using Sitecore;

    public class Helper
    {
        public string ReturnCurrentItemName()
        {
            var item = global::Sitecore.Context.Item;

            return item.Name;
        }
    }
}

Summary

As long as there is no problem changing the namespace to start with “Sitecore” then option 1 is the best choice. If for whatever reason that is not an option then go with using the global namespace alias as described in option 3.

Set Rendering Parameters in Conditional Rendering

Sitecore has a set “Set Parameters” conditional rendering rule which sets the rendering parameters as the name implies. This rule has some limitations:

  1. It will remove all existing parameters
  2. User will have to know the querystring syntax to add the correct keys and values (e.g. parameter1=value1&parameter2=value2)

The second limitation can be resolved by educating the users to use correct syntax, but there is no option to resolve the first one. There are many legitimate scenarios where you would want to set some rendering parameters and either add some more based on a condition or overwrite the value for a particular parameter.

To support this scenario I created a custom conditional rendering rule to do this:

  1. If there is already a parameter and this is not specified in the conditional rendering rule, then leave it as is.
  2. If there is already a parameter, and the same is also set in the conditional rendering rule, then overwrite it with the value from conditional rendering rule.
  3. If the parameter is only in the conditional rendering rule, add it.

I used below code for my custom action:

using Sitecore.Diagnostics;
using Sitecore.Rules.Actions;
using Sitecore.Rules.ConditionalRenderings;
using System.Linq;

namespace Jeroen.Rules.ConditionalRendering
{
    public class MergeParameterAction<T> : RuleAction<T> where T : ConditionalRenderingsRuleContext
    {
        public string Name { get; set; }

        public string Value { get; set; }

        public override void Apply(T ruleContext)
        {
            Assert.ArgumentNotNull(ruleContext, "ruleContext");

            //grab the existing parameters
            var parameters = Sitecore.Web.WebUtil.ParseUrlParameters(ruleContext.Reference.Settings.Parameters);

            //parameter already there, overwrite
            if (parameters.AllKeys.Contains(Name))
            {
                parameters[Name] = Value;
            }
            //add new parameter
            else
            {
                parameters.Add(Name, Value);
            }

            //add updated parameters back
            ruleContext.Reference.Settings.Parameters =
                string.Join("&", parameters.AllKeys.SelectMany(
                    parameters.GetValues, (n, v) => string.Format("{0}={1}", n, v)));
        }
    }
}

Below screenshot shows how to add the action:

conditional-rendering-custom-action

Now the rule is available in Sitecore and ready to use. You can use the action multiple times in case you need to set multiple parameters as shown here. Now the additional parameters will be set and existing parameters will be kept, there is also no need for the user to remember the syntax, he can just click the name/value pair in the rule editor!

conditional-rendering-using-custom-action

Set any action in conditional rendering on item through personalize screen

The Sitecore UI supports setting any action in global conditional rendering rules, however this is not possible when you try to do this on an item through the personalize button in presentation details. In this case it only supports showing or hiding the component and setting the datasource. First screenshot below shows the editor when you go to the rules in the global conditional rendering (under “/sitecore/system/Marketing Control Panel/Personalization/Rules”). The second screenshot shows the editor when you make a change on the item. Notice that the rules option are limited here.

This is less of an issue if you are using WebForms as you can just make your rule a global rule. With MVC this can become more of a problem as MVC does not support global conditional rendering rules, in MVC you are still able to select a global rule however the rule will not do anything.

Solution

It turns out that this issue is only with the Sitecore UI and any action is supported it is just not possible to select it in the UI. Conditional rendering rules get stored in the Layout of an item in the Renderings and Final Renderings fields. Usually the easiest way to update the layout with the rule is to create the rule first as a Global Rendering rule and then grab the raw value of the rule. Below is an example with a sample rule that logs a message when it’s Sunday. See rule and associated XML below:

log-message-on-sunday

<ruleset>
  <rule uid="{EE27A156-B552-43C0-AFD2-616D9AAE2846}" name="Rule 1">
    <conditions>
      <condition id="{1F15625B-8BDC-4FD2-8F0C-6EE2B8EF0389}" uid="B96A756592C04746856F5F8F9784C3E1" day="{04CC0FD2-C5DE-4F7C-B263-B1C88BABA6CD}" />
    </conditions>
    <actions>
      <action id="{4D151B8B-BD5F-4479-A35F-EE740F6387E8}" uid="5AB9220AD9604A7FABBE48853926B82C" level="Info" text="It's Sunday!" />
    </actions>
  </rule>
</ruleset>

Now copy this XML and update the Renderings or Final Renderings field with it. Here is an example how to update the field with a sample item that has 1 rendering.

Initial:

<r xmlns:p="p" xmlns:s="s" p:p="1">
  <d id="{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}">
    <r uid="{39EE95EC-3BA9-4D8A-AC3E-51DB3FBD353A}">
      
    </r>
  </d>
</r>

With conditional rendering rule added:

<r xmlns:p="p" xmlns:s="s" p:p="1">
  <d id="{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}">
    <r uid="{39EE95EC-3BA9-4D8A-AC3E-51DB3FBD353A}">
      <rls>
        <ruleset>
          <rule uid="{EE27A156-B552-43C0-AFD2-616D9AAE2846}" name="Rule 1">
            <conditions>
              <condition id="{1F15625B-8BDC-4FD2-8F0C-6EE2B8EF0389}" uid="B96A756592C04746856F5F8F9784C3E1" day="{04CC0FD2-C5DE-4F7C-B263-B1C88BABA6CD}" />
            </conditions>
            <actions>
              <action id="{4D151B8B-BD5F-4479-A35F-EE740F6387E8}" uid="5AB9220AD9604A7FABBE48853926B82C" level="Info" text="It's Sunday!" />
            </actions>
          </rule>
        </ruleset>
      </rls>
    </r>
  </d>
</r>

Notice that the difference between the initial XML and the updated one is only an additional “rls” tag with the XML from the Global conditional rendering rule inside it. Now the rule will execute as expected!

Angular 2 and Sitecore Best Practices

Angular 2 is out of beta and I have been using this for a while now to write single page applications in Sitecore. There are a couple of good blog posts around that describes how to get Angular 2 and Sitecore working. This post will focus on some touch points between Sitecore and Angular 2. The official Angular 2 site has a good article which describes setting up Angular 2 in Visual Studio 2015.

Retrieving data from Sitecore

Angular 2 has a HTTP library which makes it easy to work with JSON data returned from the server. Sitecore makes it easy to return JSON data especially from a controller action when using MVC.

However it seems like Sitecore.Services.Client is often overlooked when a restful API is required. Sitecore.Services.Client allows you to fetch Sitecore items in JSON format without writing any code, this link shows several ways to retrieve data this way. The Sitecore.Services.Client is also extensible just like almost everything else in Sitecore. The developer guide can be found here

PushState Routing

Angular 2 uses pushState routing by default, which means that the URL of the Angular route gets appended to the regular URL using a slash (/) as a separator just like Sitecore does. This is the preferred style and is good for SEO as well. An example of this would be an Angular 2 application that is hosted on mysite.com home page that has a route at mysite.com/details which will be displayed by Angular’s routing from the app on home page.

There will be an issue when someone tries to deep link into a mysite.com/details as Sitecore will try to resolve this item but cannot since this is only an Angular route not a Sitecore item. Perhaps the easiest way to solve this is by creating rewrite rules in IIS to resolve these links to the homepage. However I do not like this approach as it is not very maintainable. Creating the same app on a different page would require deploying additional rewrites. Instead I’m using below pipeline to detect this scenario and assign the correct item. Now the content author can add additional instances of the Angular 2 application without involvement from the development team:

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Pipelines.HttpRequest;
using System;

namespace Jeroen.Sitecore
{
    public class Angular2PushStateItemResolver : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (Context.Item == null && Context.Database != null && Context.Database.Name != "core")
            {
                //find all items that have the Angular app
                var query = string.Format("{0}//*[@@templateid='{1}']", args.StartPath, "<Guid of template with Angular 2 app>");
                var items = Context.Database.SelectItems(query);

                foreach (var item in items)
                {
                    var options = LinkManager.GetDefaultUrlOptions();
                    options.LanguageEmbedding = LanguageEmbedding.Never;

                    //check if this is an Angular component inside the current item and if so assign context item
                    if (args.LocalPath.StartsWith(LinkManager.GetItemUrl(item, options), StringComparison.OrdinalIgnoreCase))
                    {
                        Context.Item = item;
                        break;
                    }
                }
            }
        }
    }
}

Here is the code to include the pipeline:

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

What about SEO, Social and performance?

Angular2 is rendering the page client side which can adversely affect a number of key areas:

  • Page performance as the browser needs to download and run a good amount of javascript to run the application. This can get especially troublesome in mobile and larger more complicated apps
  • SEO as search engine bots are not able to crawl the page
  • Social as Twitter, Facebook and other social media are not able to correctly display a preview

Angular Universal will address these issues by sending the rendered output to the client. This is currently only supported in node.js and ASP .NET core but should hopefully be available for ‘regular’ ASP .NET soon. Therefore I recommend keeping the best practices from the universal team in mind to ensure the application will work in Angular universal when this is ready. Maybe Angular Universal can even get the experience editor working?!

Sitecore vary output cache by context item and datasource

Sitecore’s output cache is an excellent solution to improve the performance of a Sitecore solution. The output cache can be configured to be different for a variety of criteria as explained by John West in this post.

The most common option is the VaryByData option. I don’t think this option is documented very well, but it will vary the cache based on the datasource if it is set. The context item will be used instead if no datasource is set.

VaryByData will vary the cache based on Datasource if set, otherwise it will use the context item.

This will work well in most cases, but there are valid scenarios where the cache should vary based on the datasource and the context item. This can be avoided by adding below pipeline. This will use Sitecore’s logic to determine the cachekey, but will add the item path. The if statement can be adjusted depending on the need. Examples include setting it to the ID of the rendering that requires it or checking if rendering’s Datasource property is set if this is required for all renderings that have a datasource.

public class GenerateCacheKeyCustomized : GenerateCacheKey
{
    protected override string GenerateKey(Rendering rendering, RenderRenderingArgs args)
    {
        var cacheKey = base.GenerateKey(rendering, args);

        if (rendering.RenderingItemPath == "<your ID>")
        {
            cacheKey += string.Format("_{0}", Sitecore.Context.Item.Paths.Path);
        }

        return cacheKey;
    }
}

How to leverage Sitecore Media Request Protection in javascript/client side code

Sitecore Media Request Protection is an important feature which protects a Sitecore instance from an image resize attack. It protects Sitecore image scaling parameters by ensuring that only server generated requests are processed. This creates issues if you have any client side code that needs to leverage image scaling. For example what if you have some client side databinding that needs to show images in lower dimensions? This blog discussed 2 techniques that can be used to get around this issue while still leaving Sitecore protected from a resize attack.

1. Protect each image server side when sending image data to client

Most likely the information about the images to display will come from the Sitecore server. Instead of just sending information about the image path also already send the image scaling parameters and call ProtectAssetUrl method as described here This will add the hash value to the path so any javascript code can safely use this image URL

2. Allow known dimensions that will be requested client side

You will know in which dimensions your client side code will request images. This approach will allow all valid images dimension while all other images dimensions will only be allowed when a valid hash is provided.  Media request protection is implemented in the MediaRequest pipeline and can be customized just like almost anything else in Sitecore. Below code will check if an unsafe request is using a  ‘safe’ dimension and if so allow that:

public class CustomMediaRequest : sc.Resources.Media.MediaRequest
{
    protected override bool IsRawUrlSafe
    {
        get
        {
            bool isSafe = base.IsRawUrlSafe;

            if (!isSafe)
            {
                var safeQueryStrings = new List
                    {
                        "mh=123&mw=456",
                        "mh=654&mw=321"
                    };

                foreach (var safeQueryString in safeQueryStrings)
                {
                    if (this.InnerRequest.RawUrl.Contains(safeQueryString))
                    {
                        return true;
                    }
                }
            }

            return isSafe;
        }
    }
}

This pipeline can be included as follows:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <mediaLibrary>
      <requestParser patch:instead="*[@type='Sitecore.Resources.Media.MediaRequest, Sitecore.Kernel']" type="namespace.CustomMediaRequest,  assembly" />
    </mediaLibrary>
  </sitecore>
</configuration>

Conclusion

The first method is the better method as it uses the OOTB functionality to protect image URL’s and does not allow additional image scaling to take place. I find it hard to think of a scenario where this method will not be feasible and will recommend this for any new development. However the MediaRequest feature is still relatively new as it is introduced in Sitecore 7.5. You might need to upgrade a solution from an older version and run into many issues with a limited amount of distinct image dimensions that are requested. In this case it is probably easier and less risky to use approach 2.

Protocol relative URL not working with Sitecore Media Request Protection

Using a protocol relative URL (e.g. “<img src=”//www.mysite.com/~/media/image.png?mh=50” />) is a great way to load a resource from a different domain over HTTP when the page it is referenced on is on HTTP and over HTTPS when it is on a page which is loaded over HTTPS. You want to ensure Media Request Protection is turned on in case you want to do some scaling of the image as in the sample above to protect your Sitecore instance.

In above scenario it is easy to run into issues as the hash generated to protect the image does not handle the protocol relative url properly. Consider below hash values generated by admin page at /sitecore/admin/mediahash.aspx

  1. /~/media/image.png?mh=50 gives hash  07205019C3F1F44438D529F64842DBEE98C0632B
  2. http://www.mysite.com/~/media/image.png?mh=50 results in same hash
  3. https://www.mysite.com/~/media/image.png?mh=50 also results in same hash
  4. //www.mysite.com/~/media/image.png?mh=50 gives a different hash 5A309EF4DC195929BA7EE946A0E66AD6D349712B

I did run in a number of issues because the difference in hash value and decided against using protocol relative URL and instead always load the images over HTTPS which is working well with Media Request Protection. Recent developments also indicate that loading as much as possible over SSL is the better approach see here