fredag den 13. november 2015

Issues you might run into when upgrading Sitecore

Recently we have been working on migrating one of our customers solution from Sitecore 6.6 to 8.0 (Yes, I know 8.1 has been released, but it wasn't when we started the upgrade project).


Here is some of the issues we have run into, and what we have done to fix them.
  • Moving the items to the new master database:
The best way we have found to move the templates, media items, items etc. to the new solution, is to copy the current master database to the same database server as the new databases, and then change the connectionstring for master to point to the old database.


Then, log into Sitecore and create packages containing the things you need (remember to keep the package file size below 2GB, or Sitecore will fail to install it, since it cannot parse the zip file if is that big...) - you most likely need these items: Templates (Remember to only include your own templates, not the ones Sitecore comes with), Layouts (sublayouts, renderings etc.), System items, Media Items (make several separate packages containing these, to stay below the size limit) and content.


Once the packages has been created - change the connectionstring back, so master points to the new master database (the reason for this, is that the packages must be made from items in the database named "master" to be able to install them into the database named "master"), and install the packages - Start with the package containing the templates, so the rest of the content is supported).
  • Different DLL's from Sitecore Support:
I don't think there exists any Sitecore solution, that doesn't have at least a few DLL's from Sitecore Support (not suprising, since it is made by humans, and humans make mistakes).


However, when upgrading Sitecore, there is a good chance that some of these issues has been fixed - so start by contacting Sitecore Support, and ask them which of the Support DLL's that is still needed.


Once they have told you which one of them that is still needed - test them and see if they still works (Sitecore Support will most likely say that they works, but experience shows that they don't test them before saying that they work - so do your own testing!)
  • Some media items no longer works:
Somewhere while developing Sitecore 7.x, replacers has been changed to also work for media items.
This means, that if you for instance has a replacer that replaces " " with "-", to make URL's more SEO friendly, this now also impacts media items.


So, if there is any media items (or media folders), containing some of the characters you are replacing, they can no longer be opened by users.


An easy fix is to make a small tool that runs through the media library, looks for items with those names (remember to check display names for all languages, if useDisplayNames is set to true on the LinkManager), and then rename them.
  • Some items does not have any version (and therefore cannot be published):
When installing the items using the packages, some items might end up without any version (we had to add an version to some folders inside the Layouts folder, to be able to publish them and get the site working).


Again, make a small tool that crawls the entire solution and look for items without any version at all, and create an English version (or that ever your default language for the solution is).
  • WFFM forms:
If you have to move WFFM forms from Sitecore 6.6 to 8.0, do note that the data the users has entered will not be moved, so make sure that is okay with the customer (most likely it is old data, and okay to not move along).


You might run into some issues when moving them, but they are solution specific, so be prepared to get some help from Sitecore Support.


That is all the problems we have run into, but nothing that cannot be fixed, which is okay.
Just remember to account for it, when estimating how long the upgrade is going to take you.

tirsdag den 10. november 2015

Certain users suddenly cannot insert components using the page editor

Okay, this was an interresting issue to debug.


One of our customers was having an issue with one (and only one) of their users not being able to insert components on pages using the page editor.


First we tried comparing permissions, role membership with other users (they matched btw.), and nothing helped.


To solve it, we wrote a small aspx page, that changed the current user context to the broken user (to avoid having to ask the user for her password, since she is loaded using the AD module, getting her AD password is a big no-no).


Then, as soon as we had the user context, it was pretty easy to open the page editor, and see for ourselves.


For some reason, if the user has unchecked the Design checkbox, the New Component button is just disabled (hint to UI designes - if you disable something, add a tooltip that tells why it is disabled!).


That also explains why it only happened to her, since it is saved on her Sitecore profile.

onsdag den 4. november 2015

When the From field in WFFM is not being used

If you might happen to be running Sitecore 6.6 still, and be using WFFM for your forms, you can run into this issue.


It seems that, for some reason, the WFFM module ignores the From field defined locally on a form, and always use the one defined on the Send Email Message save action.


This has been fixed in WFFM 2.4 rev. 150619, but if you are using Sitecore 6.6, you cannot upgrade to this version of the module.


So to fix this, you need a single DLL from Sitecore Support (just ask for the file "Sitecore.Support.420165.dll", and change the Save Email Message save action to use this Assembly and Class: "Sitecore.Support.420165.dll" and "Sitecore.Support.Form.Submit.SendMessage"


They publish the save action to the web database.


Now the save action will be sending mails using the locally defined From, and only fall back to the one on the save action when needed.

fredag den 4. september 2015

How to write data from Sitecore (an other sources) to a CSV file that Excel understands

From time to time, our customers wants to export data from Sitecore to CSV, to be able to view it in Excel.

Depending on where in the world you live, this might not be an issue for you, since people in the english speaking countries will just be using ASCII and there will be no issues.

But, if you like me live in one of the nordic countries (or alot of other places), you will be running into having to deal with text with letters, such as 'æ', 'ø', 'å' among others.

If you write strings containing any of these charaters into the CSV file just like that, it will come out malformed when the file is opened in Excel.

This is due to the encoding of the file, which confuses it.

Here is how to do it the easy way:

public static class CsvWriter
{
    public static void WriteToCSVFile(string[][] dataRows, string fileName)
    {
        using (FileStream stream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None))
        {
            using (BinaryWriter writer = new BinaryWriter(stream))
            {
                WriteByteOrderMarker(writer);

                foreach (string[] dataRow in dataRows)
                {
                    WriteLineSegments(writer, dataRow);
                }
            }
        }
    }

    private static void WriteByteOrderMarker(BinaryWriter writer)
    {
        byte[] BOM = { 0xef, 0xbb, 0xbf };

        writer.Write(BOM);
        writer.Flush();
    }
            
    private static void WriteLineSegments(BinaryWriter writer, string[] lineSegments)
    {
        writer.Write(Encoding.UTF8.GetBytes(string.Join(";", lineSegments) + "\r\n"));
        writer.Flush();
    }
}

First save the data in the 2D string array, and call the WriteToCSVFile function with the filename to save the data to.

First a byte order mark is written, which tells Excel (and other applications), that the content of the file is UTF-8 encoded.
Then, when each line is written, it gets encoded, so it matches.

That way, all programs should be able to read it, as long as they understand the BOM in the start of the file :-)

When WFFM forgets to include Checkbox List fields in Send Email save actions

Okay, so one of your editors just created this great new form using the Sitecore WFFM module, and it looks great!

But then you want to make it send an email when the user submits the form - and all the sudden, the great looking form generates broken emails.

The problem is, that there is a Checkbox List field in the form, and for some weird reason, the Send Email save action does not care about that field type, so it just ignores it completly.

But, the help is near (no, not Ghostbusters this time) - just contact Sitecore support, and ask for the support package attached to the issue #442870 .

Simple and easy, right? :-)

Just upgraded, and now the page editor is broken? This might be why!

A little while ago, I finished upgrading a customers solution from Sitecore 6.4.1 to Sitecore 6.6, and all the sudden, the page editor stopped working. (The editors could not enter any text into any of the text areas)

It turns out, that there is a small note in the release notes for Sitecore 6.5 that mentions, that renderings and top level text notes should be wrapped in a div or span tag.

This can also happen if there is a sitecore placeholder placed at the top level of a usercontrol - so in this case, you also need to wrap it in a div tag.

So here you have it - it's a small thing to do, but it took quite a while to figure it out - hopefully this can save you from that, if you run into the same issue.

torsdag den 18. juni 2015

The case of WFFM going double escaping

This one is quiet interresting - a while ago, we had a customer which wanted to use the Send Email save action in WFFM.

They had designed the form with several fields, including a field called Mail, in which the user should enter their email address.

In the save action, they had selected the Mail field in the CC section, so the user would be CC on the mail that got send out.

The way this works, is when you insert a field into here, it is wrapped with square brackets, so it becomes [Mail] .

Now, when they clicked OK to close the save action, and opened it again, Sitecore had changed it to [[Mail]], which made it fail with an exception when the user filled out the form.
Changing it back and saving made not difference, since it kept changing it to [[Mail]].

We contacted Sitecore Support, and got a fix for it - so if you run in to this issue, please contact them, and say you want the fix from issue #436119.

The version we had the issue with is 2.3 rev.140617, so if you are using another version, it might work for you without the fix.

Insert Media Link in General Link breaks if parentheses is used

In Sitecore 6.6 (the issue is fixed from 7.0 and forward), there is an annoying bug, that causes the Insert Media Link dialog, if parentheses is used in the link description or the alternative text.

Here is how to reproduce it:
  1. Install Sitecore 6.6 rev. 140410
  2. Create a template with a general link field.
  3. Create an item based on this template.
  4. Click Insert Media Link, and select a media item.
  5. Enter a link description and alternative text containing parantheses, and click OK to close the dialog.
  6. Click Insert Media Link again, and watch the "Value cannot be null" exception.
If you happen to run into this issue, there is a workaround you can get from Sitecore Support - just contact them, and ask for the fix given out in the issue #434185 .

It is a small DLL and a zip file, so nothing fancy - but none the less, it works.

The case of the test server with 500MB/s reads from the web database MDF file

First, let me describe the environment.

We have these servers:

SERVER-CM, which is the sitecore content management server.
SERVER-CD, which is the sitecore content delivery server.
SERVER-SQL, which is the MSSQL server containing the databases.
SERVER-TEST, which is the test server, containing both the test databases and the test sitecore instance.

Now, when the site was developed, it was deployed to both the production and the test environment, and changes was applied both places when we make changes.

Time went by, and we had to upgrade the site, so we thought it would be a good idea to upgrade the production environment, make a copy of it, and overwrite the test environment with this copy, since it still contained the original test data, which made it difficult to test some changes, since there was no real data.

So, we upgraded the production environment, and made the copy.

The production environment performed fine, but the test environment all the sudden has a constant load of several hundred MB/s reads from the web database.

After alot of debugging, profiling and staring into the screen, we found the cause.

It turns out, when staging is enabled, everything that needs to get done on all servers is written as events into the EventQueue table in the web database - and also which instances of Sitecore that has run these events. (The EventQueue table is meant to be trimmed every 4 hours, but for some reason that does not happen, but thats another issue).

So, when all the sudden the test instance sees a copy of the web database from production, it sees 1.3 million event records that it needs to go through and replay, since it thinks it never has run them (even though it has, since it is a copy of an instance that has).

After we truncated the EventQueue table in the web database and restarted the IIS process, the database I/O went normal instantly, and performance seems to have improved.

So, if you clone an production environment into a test environment, and staging is enabled, make sure to truncate the EventQueue tables.

onsdag den 27. maj 2015

A simple usercontrol to make sure resources is refreshed when changed

Often in a project, the things evolve over time, which means both stylesheets and scripts needs to be updated.

A problem you can often run into, is that the client keeps the old version cached, which can break things in really weird ways.

This is a simple way to fix this - all you need to do is replace your script and link tags with this usercontrol.
That way, it ensures the url to the files gets a unique querystring added, which makes sure the client refreshes the file if it gets changed.

To do this, create a new class in a C# project, and call it HashedResourceFile, and paste the following code into it:

public class HashedResourceFile : WebControl
{
    public Uri ResourceUrl { get; set; }

    private string FinalUrl
    {
        get
        {
            string url = ResourceUrl.ToString();

            // Handle external URL's with not-defined protocol.
            if (url.StartsWith("//"))
            {
                return url;
            }

            // If the URL does not start with leading slash, it is external, and we don't do anything with it.
            if (!url.StartsWith("/"))
            {
                return url;
            }

            FileInfo fileInfo = new FileInfo(HttpContext.Current.Server.MapPath(url));

            if (!fileInfo.Exists)
            {
                throw new FileNotFoundException("File in resource not found: " + ResourceUrl);
            }

            string hash;

            using (MD5 md5 = MD5.Create())
            {
                using (FileStream stream = fileInfo.OpenRead())
                {
                    hash = BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", string.Empty);
                }
            }

            return ResourceUrl + "?filehash=" + hash.Substring(0, 8);
        }
    }

    private ResourceLinkType LinkType
    {
        get
        {
            if (ResourceUrl.ToString().EndsWith(".css", StringComparison.InvariantCultureIgnoreCase))
            {
                return ResourceLinkType.Css;
            }

            if (ResourceUrl.ToString().EndsWith(".js", StringComparison.InvariantCultureIgnoreCase))
            {
                return ResourceLinkType.Javascript;
            }

            return ResourceLinkType.Invalid;
        }
    }

    protected override void Render(HtmlTextWriter writer)
    {
        if (writer == null)
        {
            throw new ArgumentNullException("writer", "writer must not be null");
        }

        if (ResourceUrl == null || string.IsNullOrEmpty(ResourceUrl.ToString()))
        {
            return;
        }

        switch (LinkType)
        {
            case ResourceLinkType.Css:
                writer.Write("<link rel=\"stylesheet\" type=\"text/css\" href=\"{0}\" ", FinalUrl);

                foreach (string key in Attributes.Keys.Cast<string>().Where(key => key != "rel" && key != "type" && key != "href"))
                {
                    writer.Write("{0}=\"{1}\" ", key, Attributes[key]);
                }

                writer.Write("/>");
                break;
            case ResourceLinkType.Javascript:
                writer.Write("<script type=\"text/javascript\" src=\"{0}\" ", FinalUrl);

                foreach (string key in Attributes.Keys.Cast<string>().Where(key => key != "type" && key != "src"))
                {
                    writer.Write("{0}=\"{1}\" ", key, Attributes[key]);
                }

                writer.Write("></script>");
                break;
            default:
                return;
        }
    }

    private enum ResourceLinkType
    {
        Invalid,
        Css,
        Javascript
    }
}

Then, when you create your project, add this to the top of the aspx/ascx file:

<%@ Register TagPrefix="test" Namespace="YourNamespace" Assembly="YourDLL" %>

Then, add this instead of your css/js file references:

<test:HashedResourceFile ResourceUrl="/static/css/style.css" runat="server" />

When the page is rendered, it will add a querystring to the file, containing a hash of the file, that changes when the file changes.

Really simple solution, to a really annoying problem :-)

onsdag den 4. marts 2015

Small little trick to validate if an email address is valid, and getting them as MailAddress objects, even with IDN domain names

The world today is pretty international, which means that we can no longer assume that domain names is only ASCII.

If you try and create a MailAddress object in .NET with an IDN domain, it will fail with a FormatException.

To fix this, I have created two small helper functions.
One that gets a MailAddress object that works both with non-IDN and IDN domain names.
The other uses that one to validate if an email address is valid.

This is pretty smart, since people have a tendency to validate email addresses using different regular expressions, and none of them really works in all cases.

So first - here is how to get an email address for both types of domains:

public static MailAddress GetMailAddress(string email, string name)
{
    if (string.IsNullOrEmpty(email) || email.Contains(' ') || email.Count(c => c == '@') > 1)
    {
        return null;
    }

    try
    {
        return string.IsNullOrEmpty(name) ? new MailAddress(email) : new MailAddress(email, name);
    }
    catch (FormatException)
    {
        if (!email.Contains("@") || email.Count(c => c == '@') != 1)
        {
            return null;
        }

        string[] parts = email.Split('@');

        try
        {
            IdnMapping mapping = new IdnMapping();
            return string.IsNullOrEmpty(name) ?
                new MailAddress(string.Concat(mapping.GetAscii(parts[0]), "@", mapping.GetAscii(parts[1]))) :
                new MailAddress(string.Concat(mapping.GetAscii(parts[0]), "@", mapping.GetAscii(parts[1])), name);
        }
        catch (ArgumentException)
        {
            return null;
        }
        catch (FormatException)
        {
            return null;
        }
    }
}

And here is how to use it to validate email addresses:

public static bool ValidateEmailAddress(string email)
{
    return GetMailAddress(email, string.Empty) != null;
}

This is smart, since we don't use any regular expressions for it, but just uses .NET's own implementation to validate addresses.

That is really it - and it works everywhere, not just in Sitecore :-)

Bug in WFFM prevents number validation on text fields

There seems to be a bug in WFFM, that causes it to fail to validate entered text as valid numbers.

If you configure it to validate between 1 and 2,147484E+09, it seems to think that as between 1 and 2.

There is a solution, but you have to contact Sitecore Support to get it - if you do, mention you want the solution from the support issue #430157 .

Once the javascript file they mention has been updated, it will once again validate the values correct.

Enabling the Sitecore license ID on the logon screen for Sitecore 8

Since Sitecore 8, for some reason, it seems like the license ID is considered something confidential.

Now, it is no longer rendered by default - however, if you want to have it back at the login screen, change the following setting to false in an include file: Login.DisableLicenseInfo

Once you have done that, it can be accessed from the login screen again.

fredag den 23. januar 2015

An improved, multisite aware alias solution

Lets face it - the builtin alias functionality in Sitecore isn't really that great.
It does not support multiple sites with the same alias, and only supports replacing the Sitecore.Context.Item with the target item, not redirecting.

That is why I have created another solution, that is way more flexible, supports the following things:
  • Being multisite aware (every alias is defined for the site it is related to).
  • Handling incoming URL's that is no at the root of the site (like: "/test/test2").
  • Supporting different extensions (like .php ) as long as they are mapped to the ASP.NET ISAPI module.
  • Setting the context item (like the current alias does).
  • Redirect (both temporary and permanent) to the new target item.
All in all, it has alot of different features.

Now, lets take a look at how it is implemented - first lets talk a little bit about making multisite solutions in Sitecore.
The way we do, is creating a folder in /Sitecore/Content called Websites .
Inside here, we create a folder for each website, and in there, we create two items - an item called Global, that contains all the metadata for the site, and an item called Home, which is the frontpage for the website (with all the content pages being descendants from here).

Then for each site, we create a folder called Aliases under the Global item.

First, we have to create some dummy items, to be able to select from a dropdown list, to indicate which redirect mode we should use - so create a folder here: /sitecore/system/modules/AliasRedirectModes .

In this folder, create 3 items, based on the Standard Fields template (it has no custom fields, which is perfect for this case) :

200 - Rewrite internal URL
301 - Moved Permanently
302 - Moved Temporarily

Then create a template somewhere, for you alias, with the following fields:

Name / Type / Source
Incoming Link / Single-Line Text /
Outgoing Link / General Link /
Redirect Mode / Droplist / /sitecore/system/modules/AliasRedirectModes

Now, make sure this template is added to the insert options for the Aliases folder create under Global for your sites.

And then for some code :-)

Create a class in a Sitecore project somewhere, and paste the following code:

public class AliasItemResolver : HttpRequestProcessor
{
    public override void Process(HttpRequestArgs args)
    {
        // If the current item is found, there is no reason to go on, so we return.
        if (Sitecore.Context.Item != null)
        {
            return;
        }

        // If there is no context, we are not in a request from a web browser.
        if (args == null || args.Context == null)
        {
            // So we return.
            return;
        }

        // We have to make sure we don't run for the website to make tasks working without failing.
        if (Sitecore.Context.Site.Name == "website")
        {
            return;
        }

        // If not running on a site under /sitecore/content/websites, just return.
        if (!Sitecore.Context.Site.StartPath.StartsWith("/sitecore/content/websites", StringComparison.InvariantCultureIgnoreCase) || Sitecore.Context.Site.Name == "shell")
        {
            return;
        }

        // Get the requested URL.
        Uri url = args.Context.Request.Url;

        // We find the aliases folder for this site.
        Item aliasesFolderItem = GetAliasesFolder(Sitecore.Context.Site);

        // And then we find the right alias item.
        Item alias = (from aliasItem in aliasesFolderItem.Axes.GetDescendants()
                        where aliasItem.TemplateName == "Alias" && aliasItem["Incoming Link"].Equals(HttpUtility.UrlDecode(url.AbsolutePath), StringComparison.InvariantCultureIgnoreCase)
                        select aliasItem).FirstOrDefault();

        // If we didn't find an alias, we return.
        if (alias == null)
        {
            return;
        }

        // Get the link, and return if we fail.
        string link = GetLinkFromAlias(alias);
        if (link == null)
        {
            return;
        }

        // And then we see what kind of redirect is needed.
        switch (alias["Redirect Mode"])
        {
            case "200 - Rewrite internal URL":
                Sitecore.Context.Item = ((LinkField)alias.Fields["Outgoing Link"]).TargetItem;
                return;
            case "301 - Moved Permanently":
                // If it is 301, we redirects the client with the Moved Permanently message.
                args.Context.Response.Clear();
                args.Context.Response.Headers.Add("Location", link);
                args.Context.Response.Status = "301 - Moved Permanently";
                args.Context.Response.StatusCode = 301;
                args.Context.Response.End();
                args.AbortPipeline();
                return;
            case "302 - Moved Temporarily":
                // If it is 302, we redirects the client with the Moved Temporarily message.
                args.Context.Response.Clear();
                args.Context.Response.Headers.Add("Location", link);
                args.Context.Response.Status = "302 - Moved Temporarily";
                args.Context.Response.StatusCode = 302;
                args.Context.Response.End();
                args.AbortPipeline();
                return;
            default:
                // Unknown value, so we can't do anything.
                return;
        }
    }

    private static string GetLinkFromAlias(Item alias)
    {
        // We need to disable security, in case the item we link to is behind a login wall.
        // You might want to make sure the user is redirected to a login page, if the item being targeted is behind a login wall.
        using (new SecurityDisabler())
        {
            // TODO: HERE YOU HAVE TO FIGURE OUT HOW TO GET THE LINK FROM THE FIELD, DEPENDING ON WHAT TYPE IT IS :-)
        }
    }

    private static Item GetAliasesFolder(SiteContext site)
    {
        using (new SecurityDisabler())
        {
            // If the site is null, something is broken, and we just return.
            if (site == null)
            {
                return null;
            }

            // Then get the global folder for that site.
            Item globalFolderItem = // TODO: HERE YOU HAVE TO FIND A GOOD WAY TO FIND THE GLOBAL FOLDER ITEM IN YOUR SOLUTION :-)
            if (globalFolderItem == null)
            {
                // We couldn't find the global folder item, so we return;
                return null;
            }

            // Then we see if we can get the alias folder item, and return the folder.
            return globalFolderItem.Children["Aliases"];
        }
    }
}

I have left two small assignments in the code for you to do yourself ;-)
Now save the code, and build the project.

Then create the following include file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor type="Namespace.Classname, Assembly" patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']"/>
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

Now you are good to go - once the user creates a request, if the item is not found, it looks to see if there is an alias, and does the right action based on the found alias.

fredag den 19. december 2014

Creating media items from a .NET Stream

From time to time, we need to create media items from .NET Stream objects (mostly when a user has uploaded a file).

In this case, this little helper function makes it really easy.

The parentItem is the parent item where them media item should be created below.
The fileName is the name of the file, including the file type (like "test.txt").
The contentStream is the stream containing the file content.
The fileBased bool defines if the mediaItem should be stored in the database or in the filesystem.

public static MediaItem CreateMediaItemFromStream(Item parentItem, string fileName, Stream contentStream, bool fileBased)
{
    if (parentItem == null)
    {
        throw new ArgumentNullException("parentItem", "parentItem must not be null.");
    }

    if (string.IsNullOrEmpty(fileName))
    {
        throw new ArgumentException("fileName must not be empty", "fileName");
    }

    // Just in case forwardslash is used, we replace it with backslash, before we do the test.
    string newMediaItemName = fileName.Substring(0, fileName.LastIndexOf('.')).Replace('/', '\\');

    if (newMediaItemName.Contains("\\"))
    {
        // We end here, if the file is uploaded using an ASP.NET FileUpload control, and the IE setting
        // "Include local directory path when uploading files to a server" is enabled.
        // Then we have to remove everything up till the last backslash, to get the filename.
        newMediaItemName = newMediaItemName.Substring(newMediaItemName.LastIndexOf('\\') + 1);
    }

    MediaCreatorOptions md = new MediaCreatorOptions
    {
        Database = DatabaseHelper.MasterDatabase,
        Destination = string.Format("{0}/{1}", parentItem.Paths.FullPath, ItemUtil.ProposeValidItemName(newMediaItemName)),
        FileBased = fileBased,
        IncludeExtensionInItemName = false,
        Versioned = false
    };

    using (new SecurityDisabler())
    {
        return MediaManager.Creator.CreateFromStream(contentStream, fileName, md);
    }
}

This also handles files being uploaded from Internet Explore, when it sends it's entire file path instead of just the file name.

torsdag den 18. december 2014

The case with the ASP.NET worker process being terminated from time to time

So, the other day we ran into an interresting issue, where a customers solution was crashing the entire worker proces.

Every time this happened, a few seconds before the Sitecore log file ended - this error showed up:

5784 10:41:07 ERROR Unhandled exception detected. The ASP.NET worker process will be terminated.
Exception: System.UnauthorizedAccessException
Message: The current user does not have write access to this item. User: sitecore/Anonymous, Item: Home ({71B4ACD7-226F-47A6-913D-86A40C0F9EBC})
Source: Sitecore.Kernel
at Sitecore.Data.Items.ItemEditing.BeginEdit()
at Sitecore.Data.Engines.TemplateEngine.<>c__DisplayClass3.b__0()
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
at System.Threading.ThreadPoolWorkQueue.Dispatch()

It seem to be related to publishing some item based on a certain template.
After a while, with the help from Sitecore Support, we found the issue - it seemed one of the templates (the one the items being published was based on) was inheriting the same template twice.

This is not normally possible (you cannot select the same item multiple times in a multilist field in Sitecore) - so the field's raw value had to have been manipulated somehow.

After removing the double inheritance, it started working without any issues.
So if you run into this issue, take a look at your template inheritance - the problem might be hidden there :-)

tirsdag den 11. november 2014

Why you shouldn't just hardcode X-UA-Compatible meta tag in your layout

Sometimes, people either hardcode the X-UA-Compatible meta tag in the <head> section of the page, or in the web.config file as a custom header.

I have seen this a few times now, and every time it breaks something (especially with versions of Sitecore before 7.2) .

The problem is, that doing it this way, you prevent Sitecore from changing it when it is needed for it's own tools to work (like the page editor).

Instead, what you should do, is add the following code to the Page_Load function of your layout:

if (Sitecore.Context.PageMode.IsNormal)
{
    Response.AddHeader("X-UA-Compatible", "IE=edge,chrome=1");
}

Now, the X-UA-Compatible indication is still sent to the browser, which knows how to handle it (since it can be told this both using headers and meta tags) - but both Preview and the Page Editor works like they should, since it doesn't get set.

onsdag den 5. november 2014

Change item resolving to only find items that is in the current language

When developing solutions that has multiple languages, it is almost impossible to end up in a situation, where some of the content pages isn't translated to all the languages the website is avaliable in.

This ends up with some weird pages, that might just show stuff like "$name" in certian places (if that is what is written in the Standard Values for that field in that language).

To fix this issue, so the frontend behaves as if the page doesn't even exist in the frontend, if it isn't there in the current language, we create a small HttpRequest processor.

So, first create a new class in Visual Studio, and enter the following code:

public class EnsureItemLanguage : HttpRequestProcessor
{
    public override void Process(HttpRequestArgs args)
    {
        if (args == null)
        {
            throw new ArgumentNullException("args");
        }

        if (Sitecore.Context.Item == null || Sitecore.Context.Database == null)
        {
            return;
        }

        if (!Sitecore.Context.PageMode.IsNormal)
        {
            return;
        }

        if (Sitecore.Context.Database.Name != "master" && Sitecore.Context.Database.Name != "web")
        {
            return;
        }

        if (!Sitecore.Context.Item.Paths.FullPath.StartsWith("/sitecore/content", StringComparison.InvariantCultureIgnoreCase))
        {
            return;
        }

        if (!DoesItemExistInLanguage(Sitecore.Context.Item, Sitecore.Context.Item.Language))
        {
            Sitecore.Context.Item = null;
        }
    }

    private static bool DoesItemExistInLanguage(Item item, Language language)
    {
        if (item == null)
        {
            throw new ArgumentNullException("item");
        }

        if (language == null)
        {
            throw new ArgumentNullException("language");
        }

        return Array.Exists(item.Versions.GetVersions(true), version => version.Language == language);
    }
}

This resets the Sitecore.Context.Item to null if the current item does not exist in the current language.

To make this work, you must also create an include file, to make this load after the normal ItemResolver.

To do this, create an include config file, and add the following to it:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor type="Namespace.Classname, Assembly" patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']"/>
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

Where Namespace, Classname and Assembly has been replaced with your own values.

That is all that is needed to do this :-)

Unable to close the rich text editor in page editor mode after applying Chrome 37 patch from Sitecore

A while ago, Google updated Chrome so it no longer supports the showModalDialog javascript function, which causes it to break Sitecore on Chrome (other browsers are following along soon, so if you aren't using Chrome, you still have to think about this issue).

To see if you are affected, please take a look at this knowledgebase article, and apply it if it is needed for your Sitecore version.

When the patch have been applied however, there is a new issue that is introduced, which is that you can not close the rich text editor in the page editor by clicking the buttons.

You can edit and save just fine, but the window does not close when it should (you can use the cross in the top right corner, but that is not userfriendly to have to force the users to do this).

There is a small fix for this issue:
  1. Open the “[Website]/sitecore/shell/Controls/Rich Text Editor/EditorPage.js” file.
  2. Replace the following line in the “scCloseEditor” method:
window.close();

With:

if (top._scDialogs.length != 0) {
       top.dialogClose();
} else {
       window.close();
}

This fixes the issue, so it once again is possible to have the dialogs closing when they should.

onsdag den 22. oktober 2014

Working around Sitecore ECM 2.1 creating newsletters with identical paths in the content tree

When using the ECM 2.1 module to create newsletters in Sitecore, there seems to be an unfortunate problem.

When the newsletter is created, it is done by creating an item in Sitecore, named after the branch it is created from - and if not changed, it will stay with this name.

This is a problem, if you are inserting controls in the newsletter, that uses the path of the newsletter as it source, since it then will not be able to find the right datasource.

Already, newsletters are placed into folder, that is named after their creating timestamp, so that helps a bit on the problem - if there only is one newsletter in each folder with the same name.

But since ECM doesn't know that it should do that, there is a workaround for this - it's an ugly hack, but it works.

Create an include file, and name it so it gets loaded after Sitecore.EmailCampaign.config (like this: Sitecore.Support.422404.config - named after the support issue I got this workaround in).

Paste this into the config file, and same it:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <setting name="MaxMessageFolderSize">
        <patch:attribute name="value">1</patch:attribute>
      </setting>
    </settings>
  </sitecore>
</configuration>

Thats it - then it creates a new folder for each newsletter, which ensures there never is duplicate paths.