here and there during my Sitecore development

Latest

Limit access to Web Forms for Marketers reports

I came across this requirement when content editors are allowed to create/edit forms in the Form Designer but only some of them are allowed to view Form Reports of specific forms.

Challenges:

  • We want to allow content authors to be able to edit forms in Form Designer
  • We want to limit the access to Form Reports (users in a particular role can view a particular report i.e. users in Department A can view the report of Form A, users in Department B can view the report of Form B)

I came up with a naming convention for the role. Basically, when you create a form, you need to create a role in a specific format.

My naming format is: Form {form-name} Report Viewer
The role needs to be a member of sitecore\Sitecore Client Forms Author in order to create/edit form

For Example:
I have 3 forms
wffm01

I create 3 roles (format = Form {form-name} Report Viewer) and make them a member of sitecore\Sitecore Client Forms Author

wffm02

To control the access to Form Report dynamically, I had to override the Form Reports button. This is to check if the current user is in the role that is allowed to view the report of the current form.

namespace YourNamespace
{
    public class CustomRunFormDataViewer : Sitecore.Forms.Core.Commands.RunFormDataViewer
    {
       public override CommandState QueryState(CommandContext context)
       {
          if (!string.IsNullOrEmpty(WebUtil.GetQueryString("webform")))
          {
             return CommandState.Hidden;
          }

          var item = context.Items[0];
          string formName = item.Name;
          string roleName = string.Format("sitecore\\Form {0} Report Viewer", formName);
          if (Sitecore.Context.User.IsInRole(roleName) && item.TemplateName == "Form")
             return CommandState.Enabled;

          return CommandState.Disabled;
       }
    }
}

To use this CustomRunFormDataViewer, I replace the forms:dataviewer command in /App_Config/Include/forms.config

<!--<command name="forms:dataviewer" type="Sitecore.Forms.Core.Commands.RunFormDataViewer,Sitecore.Forms.Core" />-->
<command name="forms:dataviewer" type="YourNamespace.CustomRunFormDataViewer,CustomWffmDll" />

The following screenshot is when a user that is not in Form Apply for a Job Report Viewer clicks the Apply for a Job form item. Notice that the user can edit the form using Form Designer but cannot view the report (Form Reports button is disabled)

wffm03

That is just to disable to button. If users try to access the Form Reports from some other ways i.e. Form Reports from the start menu, we need to secure the report page as well.

wffm04

So, I added the role checking in the FormDataViewerPage itself.

namespace YourNamespace
{
    public class CustomFormDataViewerPage : Sitecore.Forms.Shell.UI.FormDataViewerPage
    {
       protected override void OnLoad(EventArgs e)
       {
          string formName = CurrentItem.Name;
          string roleName = string.Format("sitecore\\Form {0} Report Viewer", formName);
          if (!Sitecore.Context.User.IsInRole(roleName)) return;
          base.OnLoad(e);
       }
    }
}

Then I replace the default FormDataViewerPage with my CustomFormDataViewerPage in \sitecore\shell\Applications\Modules\Web Forms for Marketers\FormDataViewer.xaml.xml

<!--<Sitecore.Forms.Shell.UI.FormDataViewer x:inherits="Sitecore.Forms.Shell.UI.FormDataViewerPage,Sitecore.Forms.Core" >-->
<Sitecore.Forms.Shell.UI.FormDataViewer x:inherits="YourNamespace.CustomFormDataViewerPage,CustomWffmDll">

Now, if you select the form that you don’t have access, you will see the blank report.

Setting up TDS and Visual Studio to store configurations in multiple environments

In a very basic Sitecore solution setup in Visual Studio, there are 3 projects

  • Sitecore website (Web Application project)
  • Core database (TDS project)
  • Master database (TDS project)

When working with other developers in the team, there might be some configuration differences such as dataFolder (in web.config). Also, when moving from environment to another environment (dev, QA, staging), there will be a list of configurations you have to change. Ideally, this could be done using continuous integration and automatic deployment. However, if you don’t have that and you would like to store all configurations of all environments into the source control, you can utilize TDS using the following steps. You can also use TDS to deploy files and items to different environment.

Now, we add another project into the solution to store configurations in multiple environments

This will be a project to store environment specific setting and we will be using TDS to deploy these setting to any environment we want. Even though we’re not going to use TDS to deploy to production environment, we might want to keep production configurations in the source control so that at least we have a copy of production configurations in case we need them.

How do we set TDS to deploy to each environment?

First, start from Local. Notice that we don’t include Local folder into the project and we don’t check the local folder into the source control. Each developer will have Local folder but the configurations inside could be different

Right click the solution and open properties

Select Configuration Properties and choose Configuration Manager…

If you select the configuration dropdown, you will see environments that you have. By default you should see Debug and Release

Notice that all TDS projects have Deploy checkbox checked

We are going to configure all these TDS projects – MyWebsite.Core and MyWebsite.Master

Right click MyWebsite.Master project and open up properties

In General tab, make sure to set the Source Web Project to the MyWebsite project (web application) and point Sitecore Database to master

In Build tab, when Configuration dropdown is set to Debug mode

  • Build Output Path = .\Debug << this will tell TDS to copy files from Source Web Project that will be deployed to the target
  • You MUST check the checkbox “Edit user specific configuration (.user file) when changing Source Web Url, Sitecore Deploy Folder, Recursive Deploy Action, and Sitecore Access Guid for Debug and Release modes (developer environment) because you don’t want to save your configurations that only belong to you in the project file that we will be checking in to the source control. But, you want to save these settings into your .user file instead and you won’t be checking in this .user file

In File Replacement tab, you will set the Source Location to be Local and Target Location to be root. This will tell TDS to copy configuration files in Local folder to the target web (in wwwroot folder)

Now, try to build or deploy MyWebsite.Master, you will notice that all configuration files Local folder in EnvironmentSettings project are copied to your wwwroot

Do the same thing in Release mode for MyWebsite.Master

In MyWebsite.Core, both Debug and Release mode, you also do the same thing but except the File Replacement tab. Just leave it blank. (Note that it’s up to you as a developer. If you think you don’t do the Release mode in your local development, you don’t set anything)

Next, how do we do the same thing in Dev and Staging environments?

We will add a new mode. In solution properties, add New…

Name it Dev-Debug and copy settings from Debug mode

Notice that all project files are changed. Now a new mode Dev-Debug is added to all project files. If you’re interested, you can use a file comparison tool such as WinMerge or BeyondCompare to see the changes.

Come back to MyWebsite.Master, open up the properties

In Build tab, notice that in Configuration dropdown we now have Dev-Debug, select Dev-Debug and modify the configurations

  • Build Output Path = .\Debug << this will tell TDS to copy files from Source Web Project that will be deployed to the target
  • You DON’T check the checkbox “Edit user specific configuration (.user file)” when changing Source Web Url, Sitecore Deploy Folder, Recursive Deploy Action, and Sitecore Access Guid for Debug and Release modes (developer environment) because you want this settings to be in the project file and check into the source control so that everyone can just use these configurations on Dev server
  • VERY IMPORTANT NOTE: we should use different Sitecore Access Guid for each environment to ensure we can’t accidentally deploy to wrong environment.

In File Replacement tab, set the Source Location to copy configuration files from Dev folder to the target location (wwwroot)

Do the same thing in Release mode for MyWebsite.Master

In MyWebsite.Core, you can also do the same thing in both Debug and Release modes except the File Replacement tab, just leave it blank.

Now you should be able use Deploy functionality in TDS to deploy to development server. Note that you have to select Dev-Debug mode in Visual Studio before clicking Deploy.

In Staging and Production, you should only have build in Release mode by having Staging-Release and Production-Release. Follow the same steps to setup.

The following setup is to demonstrate how I store the configurations in this project in all environments.

Last, double check if you setup things correctly. Go through all TDS project files and .user files.

In TDS project files, you should see all configurations but not Local

In .user file, there should only be Local configuration. Each developer has own .user file with different settings.

When another developer join the team in the project, he/she will have to follow these step to setup:

  1. Install fresh Sitecore in wwwroot
  2. Get the latest of the whole solution from source control into code development folder
  3. In Configurations project after getting the latest from the source control, you will have Dev but not Local
  4. Create Local by making a copy of Dev
  5. In Visual Studio, modify the configurations in Local folder to match your environment. Don’t include this folder in the project file and don’t check into the source control
  6. Set the properties of all TDS projects. Make sure to check Edit User Specific Configuration (.user file) so that it only belongs to you.
  7. Deploy everything in Debug mode (your Debug mode should be already tied with Local folder)

Sitecore error pages not working in IIS7

I have a problem with my Sitecore website cannot display my custom error page i.e. 404. The custom error page works correctly in my local development machine but not on the server.

After trying to post a fresh installation of Sitecore website on the server, I found that the Sitecore error pages do not work on the server as well.

In the default web.config, there is a setting of ItemNotFoundUrl.


&lt;setting name=&quot;ItemNotFoundUrl&quot; value=&quot;/sitecore/service/notfound.aspx&quot;/&gt;

But, this page cannot be displayed

I found a post on StackOverflow – http://stackoverflow.com/questions/434272/iis7-overrides-customerrors-when-setting-response-statuscode

After adding existingResponse=”PassThrough” into system.webServer/httpErrors

&lt;system.webServer&gt;
     &lt;httpErrors existingResponse=&quot;PassThrough&quot; /&gt;
&lt;/system.webServer&gt;

Now Sitecore error pages show up correctly.

I can then enable my custom error pages and everything works correctly


&lt;setting name=&quot;ItemNotFoundUrl&quot; value=&quot;/custom-error-404/&quot;/&gt;

CSS failed to load in Sitecore

This issue just came up while deploying a Sitecore solution to server. CSS or JavaScript randomly fails to load. When I try to browse the CSS file in your browser, I find that the CSS can’t be browsed. In Chrome, I get Error 330 (net::ERR_CONTENT_DECODING_FAILED): Unknown error.

I know this is a problem about GZip compression. So, I unchecked Enable static content compression in IIS. And, the problem goes away. However, this is NOT the ideal solution because you don’t utilize the compression.

IIS compression

Next, I checked my Sitecore web.config. Normally, the default value for the allowed extensions is

&lt;processor type=&quot;Sitecore.Pipelines.PreprocessRequest.FilterUrlExtensions, Sitecore.Kernel&quot;&gt;
 &lt;param desc=&quot;Allowed extensions (comma separated)&quot;&gt;aspx, ashx, asmx&lt;/param&gt;
&lt;/processor&gt;

But, I want all extensions to go through Sitecore since I need to capture all other extensions such as .html, .php, etc from the old site so that I can 301 redirect properly through my custom redirect module.

&lt;processor type=&quot;Sitecore.Pipelines.PreprocessRequest.FilterUrlExtensions, Sitecore.Kernel&quot;&gt;
 &lt;param desc=&quot;Allowed extensions (comma separated)&quot;&gt;*&lt;/param&gt;
&lt;/processor&gt;

Since I allow all extensions, that means CSS and JavaScript also go through Sitecore as well. And, this is the cause of the issue.

To fix the issue that CSS doesn’t load, I will have to add the CSS/JS folder into IgnoreUrlPrefixes. Foe example, if my CSS and JS folders are “/css_folder” and “/js_folder” respectively, my IgnoreUrlPrefixes will be

&lt;setting name=&quot;IgnoreUrlPrefixes&quot; value=&quot;/sitecore/default.aspx|/trace.axd|
 /webresource.axd|/sitecore/shell/Controls/Rich Text Editor/Telerik.Web.UI.DialogHandler.aspx|
 /sitecore/shell/applications/content manager/telerik.web.ui.dialoghandler.aspx|
 /sitecore/shell/Controls/Rich Text Editor/Telerik.Web.UI.SpellCheckHandler.axd|
 /Telerik.Web.UI.WebResource.axd|/sitecore/admin/upgrade/|/layouts/testing|/css_folder/|/js_folder&quot; /&gt;

This basically allow CSS and JavaScript to load without going into Sitecore pipeline and you don’t have to disable the compression in the IIS. (You can check Enable static content compression in IIS again).


					

Sitecore deployment process in load-balanced production environment

This is the process that I take when deploying changes or new features to load-balanced content delivery servers.  The following is the basic diagram showing a normal Sitecore setup with multiple content delivery servers.

basic Sitecore scaling setup

Content Management server (CM) connects to Core, Master, and Web databases.  When a content item is published, there are 2 publishing targets available (Web and Live).  Content Delivery servers (CD1, CD2) are under a load balancer and connect to another web database (Live).  Note that CD1 and CD2 are pointing to the same database.  From a conversation with Alex Shyba (Sitecore solution architect), “No matter how many CD servers you will have, you will only have one Live database.  This is how Sitecore scaling expects.  Having multiple Live databases doesn’t help improving performance.  You can make it cluster if you want.  However, if you want to scale your website geographically multiple Live databases can be expected.  It’s highly recommended to have Live database server in the same environment as CD servers.”

The problem I have found when deploying a new feature to production environment is that sometimes the new feature requires both files and item changes (i.e. new data template, new fields, new presentation) to be deployed at the same time.   I can take CD1 out of the load balancer and deploy files to CD1 but there is no way to avoid undisruptive deployment to both CD servers if I need to publish something from Master to Live database.

The followings are deployment steps that I take (Though, there might be some other ways to accomplish the same goal).

  1. Make sure everyone is in content freeze mode.  Create a Temp database that will be used for undisruptive deployment.  Add a new publishing target “Temp” which is identical to “Live” (restored from Live backup after content freeze).  Enable Temp publishing target and disable Live publishing target.

    Undisruptive deployment - step 1

  2. Start the deployment to CD1 by taking CD1 out of load balancing.  Only CD2 is available to public via the load balancer.  Change the connectionstring of CD1 to Temp database.  Deploy the changed files to CD1 and publish the changes from Master to Temp.

    Undisruptive deployment - step 2

  3. Verify if everything works correctly on CD1.
  4. Once everything works on CD1, bring CD1 live and take CD2 out of load balancing.  At this point, the new feature is available to public successfully.  If there is anything wrong, we can easily direct the load balancer to CD2.

    Undisruptive deployment - step 4

  5. Next step, we’re going to deploy the same change to CD2 by deploying the files to CD2 and point CD2 to Temp database.

    Undisruptive deployment - step 5

  6. Once we verify if CD2 works correctly, we can add CD2 back into load balancing.  Now we have completed the deployment.

    Undisruptive deployment - step 6

  7. It depends on how you would approach this.  You can leave everything as is.  “Temp” database will now be production database.  You can name the databases as “Live1” and “Live2” if this would make more sense.  I personally want to keep everything clean.  So, I would enable Live publishing target and publish everything to live database.  Take CD1 out of load balancing and point the connectionstring to Live database.  Verify CD1 again before bringing CD1 live and take CD2 out of load balancing.  Change CD2 connectionstring to Live and verify CD2 before adding it back to load balancing.
  8. Enable Live publishing target and disable Temp publishing target.  We can keep Temp database for future deployments.  This deployment process is now complete.  We can notify content authors to get back to Sitecore again.

    Undisruptive Sitecore deployment - finished

Write XSLT efficiently in Sitecore

Even though XSLT is easy to write, it’s hard to debug and it’s more expensive when comparing to sublayout.  Anyway, if you happen to write XSLT, be sure to do it efficiently.

1. DO NOT use // in xpath – it’s very expensive to loop through all child items

2. Avoid using count() function
scenario: I just want to check if there is at least an item that matches a criteria
This code will go through all nodes, count the ones that match, then compare with 0

<xsl:if test="count($curNode/item[sc:fld('Is Featured', $curNode) = 1] ) &gt; 0">

This code will check to see if there is at least one node that matches the criteria, then stop and return true

<xsl:if test="$currNode/item[sc:fld('Is Featured', $curNode) = 1]">

3. Avoid using nested loop

4. Try to reduce number of loops
This will look through all items and check the if-statement on every single item

<xsl:for-each select="$curNode/item">
   <xsl:if test="sc:fld('Is Featured', current()) = 1">
      <!-- do something -->
   </xsl:if>
</xsl:for-each>

This will use xslt processor to select items that match the condition.  It’s faster than the first one.

<xsl:for-each select="$curNode/item[sc:fld('Is Featured', current()) = 1]">
   <!-- do something -->
</xsl:for-each>

Trigger field update with custom event handler item:saved

After spending my time trying to implement a custom event handler for one of my clients, I found one tricky bit that I didn’t expect.  I have done a fair amount of custom event handlers but this is the first time that I need to update field’s value with item:saved event.

The Sitecore template has Address, City, State, Zip, Latitude, and Longitude fields.  The Latitude and Longitude fields are used to locate a pin on Google map.  To help content authors, my job is to populate Latitude and Longitude fields automatically if we know the address.

So, the idea is to use custom event handler to generate values in latitude/longitude fields once a location item is saved

These are steps that I take

  1. Create a new custom event handler class
    First, let’s come up with a pseudo code

    • once an item is saved, check if the content template is “location”
    • get the address, city, state, zip, country fields
    • call Google Map API to get latitude/longitude
    • if there is no error, update Latitude/Longitude field in Sitecore and notify Sitecore user.

    Here’s the code

    namespace MyProject
    {
    public class LocationItemEventHandler
    {
    /// <summary>
    /// This custom event auto-populates latitude/longitude co-ordinate when a location item is saved
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    protected void OnItemSaved(object sender, EventArgs args)
    {
    var item = Event.ExtractParameter(args, 0) as Item;
    
    if (item != null)
    {
    if (item.TemplateID.Equals(Constants.LocationTemplateId))
    {
    string errMessage = "";
    string responseCode = "";
    string address = item.Fields["Street Address"].Value;
    string city = item.Fields["City"].Value;
    string state = item.Fields["State"].Value;
    string zipCode = item.Fields["Zip Code"].Value;
    string country = item.Fields["Country"].Value;
    var coordinate = GeocodeUtility.GetCoordinates(out errMessage, out responseCode,
    Constants.GoogleMapAPIKey, address, city, state,
    zipCode, country);
    if (responseCode == "200")
    {
    string latitude = coordinate.Latitude.ToString();
    string longitude = coordinate.Longitude.ToString();
    
    try
    {
    var latitudeField = item.Fields["Latitude"];
    var longitudeField = item.Fields["Longitude"];
    using (new SecurityDisabler())
    {
    item.Editing.BeginEdit();
    latitudeField.SetValue(latitude, true);
    longitudeField.SetValue(longitude, true);
    Sitecore.Context.ClientPage.ClientResponse.Alert(
    string.Format(
    "Fields updated automatically\r\nLatitude: {0}\r\nLongitude: {1}",
    latitude, longitude));
    item.Editing.EndEdit();
    }
    }
    catch (Exception exception)
    {
    Log.Error(exception.Message, this);
    }
    }
    }
    }
    }
    }
    }
    
  2. Bind this custom event handler in item:saved event in the web.config. Note that if you have multiple Sitecore instances (Content Management/Content Delivery), you only need to update Content Management web.config since item:saved event won’t take place in Content Delivery anyway.
    <event name="item:saved">
    <handler type="Sitecore.Links.ItemEventHandler, Sitecore.Kernel" method="OnItemSaved" />
    <handler type="Sitecore.Tasks.ItemEventHandler, Sitecore.Kernel" method="OnItemSaved" />
    <handler type="Sitecore.Globalization.ItemEventHandler, Sitecore.Kernel" method="OnItemSaved" />
    <handler type="Sitecore.Rules.ItemEventHandler, Sitecore.Kernel" method="OnItemSaved" />
    <!-- custom event handler for location -->
    <handler type="MyProject.LocationItemEventHandler, MyAssemblyName" method="OnItemSaved" />
    </event>
    

After testing, I found out that I ran into an infinite loop :)  Basically, when I update Latitude and Longitude fields, another item:saved event occurs. To solve this issue, I used SynchronizedCollection to keep track of item.  If the collection contains the item, exit the loop immediately.  If not, add item to the collection, update fields, and then remove the item from the collection

Here’s the updated code

namespace MyProject
{
public class LocationItemEventHandler
{
private static readonly SynchronizedCollection<ID> MProcess = new SynchronizedCollection<ID>();

/// <summary>
/// This custom event auto-populates latitude/longitude co-ordinate when a location item is saved
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
protected void OnItemSaved(object sender, EventArgs args)
{
var item = Event.ExtractParameter(args, 0) as Item;

if (item != null && !MProcess.Contains(item.ID))
{
if (item.TemplateID.Equals(Constants.LocationTemplateId))
{
string errMessage = "";
string responseCode = "";
string address = item.Fields["Street Address"].Value;
string city = item.Fields["City"].Value;
string state = item.Fields["State"].Value;
string zipCode = item.Fields["Zip Code"].Value;
string country = item.Fields["Country"].Value;
var coordinate = GeocodeUtility.GetCoordinates(out errMessage, out responseCode,
Constants.GoogleMapAPIKey, address, city, state,
zipCode, country);
if (responseCode == "200")
{
string latitude = coordinate.Latitude.ToString();
string longitude = coordinate.Longitude.ToString();

MProcess.Add(item.ID);

try
{
var latitudeField = item.Fields["Latitude"];
var longitudeField = item.Fields["Longitude"];
using (new SecurityDisabler())
{
item.Editing.BeginEdit();
latitudeField.SetValue(latitude, true);
longitudeField.SetValue(longitude, true);
Sitecore.Context.ClientPage.ClientResponse.Alert(
string.Format(
"Fields updated automatically\r\nLatitude: {0}\r\nLongitude: {1}",
latitude, longitude));
item.Editing.EndEdit();
}
}
catch (Exception exception)
{
Log.Error(exception.Message, this);
}
finally
{
MProcess.Remove(item.ID);
}
}
}
}
}
}
}