Friday 23 September 2016

Customizing PDF download behaviour for Sitecore multisite implementation

Leave a Comment
Recently I got a requirement from my client where I have to customize PDF rendering behaviour for Sitecore multisite implementation. For few websites, PDF files should be rendered in web browser itself and for few websites, web browser should prompt the user to save the PDF files. I am working with Sitecore 8.2 initial release.

PDF rendering behaviour is being managed by <forceDownload> element for PDF file media type under \mediaLibrary\mediaTypes configuration node. This setting can be found in Sitecore.Config file in \Website\App_Config folder. A true value for the forceDownload element causes Sitecore to apply an HTTP "Content-Disposition = attachment; filename=" header when linking to the .ashx URL of the media item, causing the browser to prompt the user to open/save as rather than opening the media item in the browser.


Disabling/Enabling forceDownload behaviour for PDF file will impact all the websites but I need flexibility to disable/enable this behaviour across multiple websites. This behaviour can be achieved in two ways:
  1. Forcing download on client side using HTML 5 download attribute
  2. Override MediaRequestHandler and modify "Content-Disposition" in context response header
  1. Forcing download on client side using HTML 5 download attribute
    Use HTML5 download attribute on your anchor links. The download attribute specifies that the target will be downloaded when a user clicks on the hyperlink. However this attribute is not supported by all the browsers.
  2. Override MediaRequestHandler and modify "Content-Disposition" in context response header
    Below are the steps to implement the desired behaviour across multiple websites:
    • Create a new class library project in visual studio.
    • Add reference to Sitecore.Kernel.dll
    • Add a custom attribute in your site definition. For example, "PdfForceDownloadDisabled"
      <site name="mysite" patch:before="site[@name='website']"
                  virtualFolder="/"
                  physicalFolder="/"
                  rootPath="/sitecore/content"
                  startItem="/MySite"
                  database="web"
                  domain="extranet"
                  allowDebug="true"
                  cacheHtml="true"
                  htmlCacheSize="50MB"
                  enablePreview="true"
                  enableWebEdit="true"
                  enableDebugger="true"
           PdfForceDownloadDisabled="true"
           hostName="local.mysite.com"
                  disableClientData="false"/>
    • Implement a custom Extension method :
      using Sitecore.Diagnostics;
      using Sitecore.Sites;
      using System;
      
      namespace CustomMedia
      {
          public static class UtilityManager
          {
              public static bool PdfForceDownloadDisabled(this SiteContext site)
              {
                  Assert.IsNotNull(site, "Site cannot be null");
      
                  try
                  {               
                      string PdfForceDownloadDisabled = site.Properties["PdfForceDownloadDisabled"];
                      if (!String.IsNullOrEmpty(PdfForceDownloadDisabled))
                          return Convert.ToBoolean(PdfForceDownloadDisabled); 
                  }
                  catch (Exception)
                  {
                      return false;
                  }
                  return false;
              }       
          }
      }
      
    • Override "protected override bool DoProcessRequest(HttpContext context, MediaRequest request, Sitecore.Resources.Media.Media media)" method and modify "Content-Disposition" in context response header.
      using Sitecore;
      using Sitecore.Configuration;
      using Sitecore.Data.Items;
      using Sitecore.Diagnostics;
      using Sitecore.Events;
      using Sitecore.Resources.Media;
      using Sitecore.Resources.Media.Streaming;
      using Sitecore.Web;
      using System.Web;
      
      namespace CustomMedia
      {
          public class MediaRequestHandler : Sitecore.Resources.Media.MediaRequestHandler
          {
              protected override bool DoProcessRequest(HttpContext context, MediaRequest request, Sitecore.Resources.Media.Media media)
              {
                  Assert.ArgumentNotNull(context, "context");
                  Assert.ArgumentNotNull(request, "request");
                  Assert.ArgumentNotNull(media, "media");
                  if (this.Modified(context, media, request.Options) == Tristate.False)
                  {
                      Event.RaiseEvent("media:request", new object[] { request });
                      this.SendMediaHeaders(media, context);
                      context.Response.StatusCode = 0x130;
                      return true;
                  }
                  this.ProcessImageDimensions(request, media);
      
                  MediaStream mediaStream = media.GetStream(request.Options);
      
                  if (mediaStream == null)
                  {
                      return false;
                  }
                  Event.RaiseEvent("media:request", new object[] { request });
                  if (Settings.Media.EnableRangeRetrievalRequest && Settings.Media.CachingEnabled)
                  {
                      using (mediaStream)
                      {
                          this.SendMediaHeaders(media, context);
                          new RangeRetrievalResponse(RangeRetrievalRequest.BuildRequest(context, media), mediaStream).ExecuteRequest(context);
                          //Update Header   - Custom Add             
                          this.UpdateHeader(media, context, mediaStream);
                          return true;
                      }
                  }
                  this.SendMediaHeaders(media, context);
                  this.SendStreamHeaders(mediaStream, context);
                  using (mediaStream)
                  {
                      context.Response.AddHeader("Content-Length", ((long)mediaStream.Stream.Length).ToString());
                      WebUtil.TransmitStream(mediaStream.Stream, context.Response, Settings.Media.StreamBufferSize);
                  }
                  return true;
              }
      
              private void ProcessImageDimensions(MediaRequest request, Sitecore.Resources.Media.Media media)
              {
                  int num;
                  int num2;
                  Assert.ArgumentNotNull(request, "request");
                  Assert.ArgumentNotNull(media, "media");
                  Item innerItem = media.MediaData.MediaItem.InnerItem;
                  int.TryParse(innerItem["Height"], out num2);
                  int.TryParse(innerItem["Width"], out num);
                  bool flag = false;
                  int maxHeight = Settings.Media.Resizing.MaxHeight;
                  if ((maxHeight != 0) && (request.Options.Height > System.Math.Max(maxHeight, num2)))
                  {
                      flag = true;
                      request.Options.Height = System.Math.Max(maxHeight, num2);
                  }
                  int maxWidth = Settings.Media.Resizing.MaxWidth;
                  if ((maxWidth != 0) && (request.Options.Width > System.Math.Max(maxWidth, num)))
                  {
                      flag = true;
                      request.Options.Width = System.Math.Max(maxWidth, num);
                  }
                  if (flag)
                  {
                      Log.Warn($"Requested image exceeds allowed size limits. Requested URL:{request.InnerRequest.RawUrl}", this);
                  }
              }
      
              private void UpdateHeader(Sitecore.Resources.Media.Media media, HttpContext context, MediaStream stream)
              {
                  if (media.Extension.ToLower() == "pdf" && Sitecore.Context.Site.PdfForceDownloadDisabled())
                  {
                      context.Response.Headers["Content-Disposition"] = "inline; filename=\"" + stream.FileName + "\"";
                  }
              }
          }
      }
      
    • In web.config, replace <add verb="*" path="sitecore_media.ashx" type="Sitecore.Resources.Media.MediaRequestHandler, Sitecore.Kernel" name="Sitecore.MediaRequestHandler" />  with the following line:
      <add verb="*" path="sitecore_media.ashx" type="CustomMedia.MediaRequestHandler, {your assembly name}" name="Sitecore.MediaRequestHandler" />
    • Deploy your assembly in bin folder of your Sitecore website.
Comments and suggestions are most welcome. Happy coding!
Read More...

Tuesday 20 September 2016

Sitecore Commerce Server Manager : Profile Definition 404 Error

Leave a Comment
I’ve been working with Sitecore commerce server manager for a while and recently I have stuck with one error related to Sitecore commerce server manager. I was unable to navigate through Profile Definitions in commerce server manager. I was getting HTTP 404 The webpage cannot be found error.


I’ve checked stack trace in fiddler and found out that http://<server-name>/widgets/profilebldrHTC/ProfileEdit.htm was throwing 404 error.


It seems that Sitecore Commerce Server creates virtual folder named Widgets in the IIS default website. This virtual folder points to C:\Program Files (x86)\Commerce Server 11\Widgets directory.

Follow below steps to resolve the 404 issue:
  1. Open IIS.
  2. Navigate to default website in IIS.
  3. Right click on default website and click Add Virtual Directory.
  4. Enter Widgets in Alias section. 
  5. Enter physical path as C:\Program Files (x86)\Commerce Server 11\Widgets
  6. Click OK.
  7.  Restart default website and refresh Sitecore commerce server manager. 404 error should have been gone now.
Comments and suggestions are most welcome. Happy coding!
Read More...

Saturday 10 September 2016

Ajax call in Sitecore without registering routes

Leave a Comment
I have already blogged about How to make Ajax call in Sitecore MVC where I was registering routes in RouteConfig. In this blog post I am going to explain how to make Ajax call in Sitecore MVC without registering routes in RouteConfig. This method is relatively very easy with less code changes. I am using Sitecore 8.2 initial release version for this blog post.

I’ve created a data template in Sitecore which is having three fields as shown in figure.


I’ve created few items based on newly created data template. See below figure:


I’ve created a basic controller rendering which is displaying books in a dropdown list. Below is the code of controller rendering for your reference:

Controller Code

public class BookDetailsController : SitecoreController
    {       
        public override ActionResult Index()
        {           
            List<SelectListItem> bookItems = new List<SelectListItem>();
            bookItems.Add(new SelectListItem { Text = "--Select Book--", Value = ""});
            bookItems.Add(new SelectListItem { Text = "Learn ASP", Value = "{EC2B22FE-8A6E-431F-8114-6B2944AE81B8}" });
            bookItems.Add(new SelectListItem { Text = "Learn MVC", Value = "{EBB9F7D5-22DB-4B7A-A275-42D9B5C88715}" });
            bookItems.Add(new SelectListItem { Text = "Learn SITECORE", Value = "{C89AE332-9845-4379-8C45-E8D58AAA5685}" });
            ViewBag.Books = bookItems;          
            return View();
        }    
}  

MVC View Code

Select Any Book :
@Html.DropDownList("Books")

<div id="BookDetail" style="white-space: pre;">

</div>
On change event of dropdown list; I’ll display book details (book title, book author and book language) of selected book. I’ll use AJAX to achieve this functionality.

Create a MVC controller action: I’ve written GetBookDetails action with [HttpPost] attribute in BookDetails Controller where I am passing Sitecore item id of selected book as input parameter. I’ll get book details by using item id of book item and return book details as JsonResult.
 [HttpPost]
        public JsonResult GetBookDetails(string itemId)
        {
            Book book = new Book();
            if (Sitecore.Data.ID.IsID(itemId))
            {
                Item item = Sitecore.Context.Database.GetItem(Sitecore.Data.ID.Parse(itemId));
                if (item != null)
                {
                    book.BookTitle = item.Fields["Book Title"].Value;
                    book.BookAuthor = item.Fields["Author"].Value;
                    book.BookLanguage = item.Fields["Language"].Value;
                }
            }
            return Json(book);
        }
public class Book
    {
        public string BookTitle { get; set; }
        public string BookAuthor { get; set; }
        public string BookLanguage { get; set; }
    } 
Implement AJAX call: I’ll use jQuery to make Ajax call. In the below code, I am reading the value of selected book from dropdown list and passing book item id as an input parameter while making Ajax call to get book details. Use the returned JsonResult set to update the HTML div BookDetail. Notice the value of url while making ajax request.
url: "api/Sitecore/BookDetails/GetBookDetails"
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.1.js"> </script>
<script type="text/javascript">
    $(function () {
        $("#Books").bind("keyup change", function () {
            var itemId = $(this).val();
            if (itemId != "") {
                $.ajax({
                    url: "api/Sitecore/BookDetails/GetBookDetails",
                    type: "POST",
                    data: { itemId: itemId },
                    context: this,
                    success: function (data) {
                        var BookString = "Book Title: " + data.BookTitle + "\n" + "Book Author: " + data.BookAuthor + "\n" + "Book Language:" + data.BookLanguage;
                        $("#BookDetail").text(BookString);
                        console.log("success", data);
                    },
                    error: function (data) {
                        console.log("error", data);
                    }
                });
            }
            else {
                $("#BookDetail").text("");
            }
        });
    });
</script>
Related read:
Comments and suggestions are most welcome. Happy coding!
Read More...