all things Sitecore related

Protect your Sitecore media and measure them with DMS

In a recent project I was faced with the following requirements:

  • Downloadable whitepapers from the Sitecore media library should be protected from anonymous visitors
  • Visitors should be teased to create an account or login
  • Whitepaper downloads should be measured

I first started by protecting my whitepapers in the media library. The only thing I have to do is protect my whitepapers for anonymous users. Off course I don’t want to set security on each item in the media library and that’s why I created a “protected” folder. I the media library I created a folder whitepapers and the only thing I had to do was disable inheritance for extranet\anonymous role on this folder. By doing this I actually disable default Sitecore security behavior: by default the everyone role has read access to the Sitecore top level item. Via inheritance this read access right is applicable to all items in the three. By disabling inheritance for the role extranet\anonymous on the whitepaper folder this default behavior is not applicable, meaning that member of extranet\anonymous are not able to read the whitepaper folder and all descendants. We can see this very clear with the access viewer:

accessviewer

access viewer

Next step is creating a sublayout which can render my whitepaper. What I did was render two kind of urls: when visitor doesn’t have read rights (because they are member of extranet\anonymous) I render a link to a login/create user page. If the visitor has read rights then I will supply a visitor with a download link.

My sublayout contains a repeater which populates all children from the whitepaper media folder. To retrieve those items I did the retrieval in a securitydisabler, this is necessary because otherwise my Sitecore query won’t return any items because of the security I previous configured. My code to set the datasource for the repeater would be something like:


using (new SecurityDisabler())
{
rpt.DataSource = Sitecore.Context.Database.GetItem("/sitecore/media library/Whitepapers").Children;
}

Then let me show what I added to the item template from the repeater:


<ItemTemplate>
<li>
<asp:PlaceHolder runat="server" Visible="<%#IsDownloadable((Item)Container.DataItem) %>">
<a href='<%# string.Format("/securedmedia/file?id={0}", ((Item)Container.DataItem).ID) %>'>
<%#((Item)Container.DataItem).Name %>"
</a>
</asp:PlaceHolder>
<asp:PlaceHolder runat="server" Visible="<%#!IsDownloadable((Item)Container.DataItem) %>">
<a href='/registerform'>
<%#((Item)Container.DataItem).Name %>"
</a>
</asp:PlaceHolder>
</li>
</ItemTemplate>

As stated earlier we have two link variations: a download or a register/login link depending if we are allowed to download a specific file. The download link looks perhaps a little bit strange but the reason to do it this way is because I want the download via a handler so I can trigger a page event in the analytics database and do some other coding.
The logic to determine which link to render I added in the sublayout code behind file in the IsDownloadable method. The code is:


protected bool IsDownloadable(Item pFile)
{
MediaItem lMediaItem = null;
if (pFile.Paths.IsMediaItem)
{
lMediaItem = pFile;
if (lMediaItem != null &amp;&amp; lMediaItem.InnerItem != null)
{
if (lMediaItem.InnerItem.Security.CanRead(Sitecore.Context.User))
{
return true;
}
}
}
return false;
}

The code is very straight forwarded by using the Sitecore API to check security on a specific item.
Now I have to create 2 things, a page for register/login and a handler to provide the file download. The register/login page is just a simple page which I created with two webforms for marketers forms. I used the default save actions “user login” and “create user”.
Then the file download. For that I use a handler, mainly to do the download and register a page event in the analytics database to store which whitepaper is downloaded. As you can see in the previous description of the sublayout definition the url for downloading files contains “/securedmedia/” that’s the part I use to define my handler. I register my handler in the web.config handler section:


<add name="MediaRetrievalHandler" verb="*" path="/securedmedia/*" type="Website.MediaRetrievalHandler" />

This means that when a link contains /securedmedia/ it will be processed by the mediaretrievalhandler. The implementation of this handler will look like this:


public class MediaRetrievalHandler : IHttpHandler, IRequiresSessionState
{
public void ProcessRequest(HttpContext pCtx)
{
string lFileId = WebUtil.GetQueryString("id");
if (!string.IsNullOrEmpty(lFileId) &amp;&amp; MainUtil.IsID(lFileId))
{
var lDb = Sitecore.Configuration.Factory.GetDatabase("web");
var lMediaItem = lDb.GetItem(new ID(lFileId));
if (lMediaItem != null)
{
if (lMediaItem.Paths.IsMediaItem)
{
lMediaItem = new MediaItem(lMediaItem);
using (new SiteContextSwitcher(SiteContextFactory.GetSiteContext("website")))
{
Tracker.IsActive = true;
if (Tracker.IsActive &amp;&amp; Tracker.CurrentPage != null)
{
var lPageEventData = new PageEventData("download whitepaper")
{
DataKey = "download whitepaper",
Data = lMediaItem.Name,
Text = lMediaItem.Name
};
Tracker.CurrentPage.Register(lPageEventData);
Tracker.Submit();
}
}
WriteFile(pCtx, lMediaItem);
return;
}
}

The writefile method is actually responsible for downloading the file and if not authorized it will throw an unauthorized exception. I don’t have to do anything special for this I can just rely on the Sitecore security I configured earlier:


private void WriteFile(HttpContext pCtx, MediaItem pFile)
{
try
{
string lContentType = null;
switch (pFile.Extension)
{
case "pdf":
lContentType = "application/pdf";
break;
default:
lContentType = "application/octet-stream";
break;
}
pCtx.Response.Clear();
pCtx.Response.StatusCode = 200;
pCtx.Response.Cache.SetExpires(DateTime.Now.Add(TimeSpan.FromDays(7)));
pCtx.Response.Cache.SetCacheability(HttpCacheability.Public);
pCtx.Response.Cache.SetValidUntilExpires(false);
pCtx.Response.Cache.SetLastModified(DateTime.Now);
pCtx.Response.ContentType = lContentType;
pCtx.Response.AddHeader("content-disposition", "attachment; filename=" + pFile.Name + "." + pFile.Extension);
pCtx.Response.Flush();
WebUtil.TransmitStream(pFile.GetMediaStream(), pCtx.Response, Sitecore.Configuration.Settings.Media.StreamBufferSize);
pCtx.Response.End();
}
catch (UnauthorizedAccessException lEx)
{
Log.ErrorFormat(lEx, "Error returning file {0}", pFile.Path);
pCtx.Response.StatusCode = 401;
pCtx.Response.StatusDescription = "Unauthorized";
}
catch (Exception lEx)
{
Log.Error(string.Format("Unable to write file {0} to the response", pFile.Path), lEx);
pCtx.Response.StatusCode = 404;
pCtx.Response.StatusDescription = "File not found"
}
}

So now we have almost everything ready:

  • Created a sublayout to download the whitepapers
  • Created the code to download the whitepapers and register appropriate page events
  • Configured Sitecore security to prohibit unauthorized visitors from downloading
  • Create a page with webforms to authorize or register

One thing I also did: in this case we let visitors authorize or register themselves as a Sitecore user (in my case I did it via a custom created domain). This is very valuable to use in DMS scenario’s even in mine. What I want is to couple this Sitecore user to the visitor to get more information. Later on in reporting whitepaper downloads I can supply not only visitor information but also Sitecore user information. To accomplish this I have to set the externaluser property from the visitor. This can be done with:
Tracker.Visitor.ExternalUser = Sitecore.Context.User.Name;
I am not quite sure where to put this code but I could imagine doing this in a pipeline 😉
Now that we have done this, functionality on the website is ready and we can extract the result out of our analytics database. I have to simple examples:

1. API
With the analytics API we can retrieve the page events from a specific visitor and we can count how many times he has downloaded a certain whitepaper:


StringBuilder lSb = new StringBuilder()
Visitor.LoadAll(VisitLoadOptions.PageEvents, VisitorOptions.Visitor);
var lEvents = Visitor.DataSet.PageEvents.Where(lE => lE.DataKey.Equals("download whitepaper"));
var lGroups = lEvents.GroupBy(n => n.Data).Select(group =>
new { Name = group.Key, Count = group.Count() }).ToList();
if (lGroups.Any())
{
foreach (var lGroup in lGroups)
{
lSb.AppendFormat("whitepaper: {0} downloads: ", lGroup.Name, lGroup.Count).AppendLine();
}
}

2. Report
Creating an engagement analytics report to show the whitepaper downloads. Because I have no stimulsoft knowledge and no report designer software, I decided to copy an existing report, changed only some labels and created a new SQL query to bind to the report. In my case I copied the latest failures report did some minor customization. After that I registered in Sitecore:

report

report definition

Then I created the query:

reportquery

report query

And that will do the trick:

engagementanalytics

engagement analytics report

So that’s it. If you have any questions or remarks on this please let me know.

Ronald.

Tags: , ,

About the Author

About the Author: I am working as a Team leader at IQuality Business Solutions B.V., specialized in solution development, with a strong focus on Sitecore. Although most of my time busy with developing, I am also making designs, give sitecore training and advise customers. I am Sitecore MVP since 2011. You can follow me on twitter: @rhlnieuwenhuis please also visit our company sitecore subsite on: getsmarterwithsitecore .

Subscribe

If you enjoyed this article, subscribe now to receive more just like it.

There is 1 Brilliant Comment

Trackback URL | Comments RSS Feed

  1. Phani says:

    Hi, I didn’t understand the need to call the Sitecore “canRead” security API. By default, even if the user knows the path and doesn’t have access rights to the item, then sitecore returns null when we call GetItem(s). I personally removed extranet\Anonymous access and enabled read access for our own Role. Could you please correct me if I am wrong. Thanks.

Post a Comment

Your email address will not be published. Required fields are marked *

Top