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.