Monday, 18 March 2013

Facebook, Internet Explorer, Anti-Forgery Tokens and Cookies

Tricky issue with the above 4 in one app...

IE was blocking a session cookie from my ASP.Net MVC web application when hosted in the Facebook IFRAME. The anti-forgery token in ASP.Net (used to protected against spoof form posts known as CSRF attacks) would fail saying the cookie it was checking against couldn't be found.

Turns out the issue was that in medium security settings, IE will "block third-party cookies that do not have a compact privacy policy". And as the app is in the IFRAME it is considered third party with respect to Facebook.

To resolve I needed two things:

1) an XML file located at /w3c/p3p.xml containing

<META xmlns="http://www.w3.org/2002/01/P3Pv1">
  <POLICY-REFERENCES>
    <EXPIRY max-age="10000000"/>
  </POLICY-REFERENCES>
</META>

2) and a header emitted (in server side code, the meta tag equivalent didn't seem to suffice)

Response.AppendHeader("P3P", "CP='IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA'");

At some point should really make sure that the file and header actually reflect what the privacy policy is... but for the issue at hand just having the header and XML present suffices. If you then go View > Website Privacy Policy... you should find the cookies are no longer blocked.

Friday, 8 March 2013

Localised Form Validation in ASP.Net MVC

I've recently been working on a web application that required localisation - the display of translated copy depending on which country the user was visiting from. There was a different sub-domain for each country which was used to identify the appropriate one.

It was also a requirement that the editor's of the website could update the translated copy from a simple back-office tool.

For most of the copy of the website this was fairly straightforward to set up. I had a database table called Texts that contained three columns: Country, Key and Value. And hence by looking up for a particular country and key I could get the text to display.

To ensure this was performant in practice what I did for each web request was to look up all the keys and values for the appropriate market and populate a dictionary object. The look-up was cached using so I was only hitting the database when I needed to rather than on every request.

I then created a base view model that all other view models were derived from that looked like this:

    public abstract class BaseViewModel
    {
        public BaseViewModel(IList<Text> dictionary)
        {
            Dictionary = dictionary;
        }

        public IList<Text> Dictionary { get; set; }

        public string GetText(string key)
        {
            if (Dictionary != null && !string.IsNullOrEmpty(key))
            {
                var text = Dictionary.Where(x => x.Key == key).SingleOrDefault();
                if (text != null)
                {
                    return text.Value;
                }
            }

            return string.Empty;
        }
    }

With this in place, rendering the appropriate text from the view was a simple matter of calling the GetText method and passing the required key:

    @Model.GetText("copyright")

All very straightforward so far, but the piece that needed a bit more thought was how to handle form validation messages, supporting both client and server side methods. There's a fairly well established method using resource files, but in my case that wasn't ideal as I wanted to support the editor's making amends to the texts, and hence needed to have them in the database rather than baked into a resource.

First step was to apply the usual data annotations, but rather than hard coding the error messages I instead used the keys from my translations table, e.g.

    [Required(ErrorMessage = "contact_validation_first_name_required")]
    [StringLength(20, ErrorMessage = "contact_validation_first_name_length")]
    public string FirstName { get; set; }

In my view I set up the field as follows:

    
    @Html.EditorFor(m => m.FirstName)
    @Html.LocalizedValidationMessageFor(m => m.FirstName, Model.Dictionary)

LocalizedValidationMessageFor was a custom helper used to render the appropriate error message for the country, and was implemented as an extension method on the HtmlHelper:

    public static class LocalizationHelpers
    {
        public static MvcHtmlString LocalizedValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IList<Text> dictionary)
        {
            // Get HTML returned from base ValidationMessageFor method
            var html = ValidationExtensions.ValidationMessageFor(htmlHelper, expression);

            // Check we are looking at one that is flagging a validation error
            if (html != null && dictionary != null && html.ToString().Contains("field-validation-error"))
            {

                // Get the key from the HTML (it contains a single span tag)
                var key = html.Substring(html.IndexOf(">") + 1).Replace("</span>", string.Empty);

  // Look up the text value based on the key from the passed dictionary
                var text = dictionary.Where(x => x.Key == key).SingleOrDefault();
                if (text != null)
                {

          // Replace the key with the translation
                    var amendedHtml = html.ToString().Replace(key, text.Value);
                    return MvcHtmlString.Create(amendedHtml);
                }
            }

            return html;            
        }
    }

That worked nicely for server side validation messages, but the client side ones would still display the key rather than the translated text. To handle this scenario I added a further method the BaseViewModel which would return the part of the dictionary of key/value pairs as a JSON result:

    public string GetDictionaryAsJson(string stem = "")
    {
        if (Dictionary != null)
        {
            var serializer = new JavaScriptSerializer();
            return serializer.Serialize(Dictionary
                .Where(x => string.IsNullOrEmpty(stem) || x.Key.StartsWith(stem))
                .Select(x => new { Key = x.Key, Value = x.Value }));
        }

        return string.Empty;
    }

From within the view, I made a call to a javascript function, passing in this dictionary in JSON format:

    localizeClientSideValidationMessages(@Html.Raw(Model.GetDictionaryAsJson("contact_validation")));

That function looked like the following, where for each tye of validation I was using (required, string length etc.) the key that was currently rendered was replaced with the appropriate translated text retrieved from the dictionary:

    function localizeClientSideValidationMessages(dictionary) {

        // Convert to JSON arry
        dictionary = eval(dictionary);

        // Localize fields (need to call for each type of validation)
        localizeFieldValidation(dictionary, "data-val-required");
        localizeFieldValidation(dictionary, "data-val-length");
        localizeFieldValidation(dictionary, "data-val-regex");
        localizeFieldValidation(dictionary, "data-val-equalto");

        // Reparse form (necessary to ensure updates)
        $("form").removeData("validator");
        $("form").removeData("unobtrusiveValidation");
        $.validator.unobtrusive.parse("form");

    }

    function localizeFieldValidation(dictionary, validationAttribute) {
       
        // For each form element with validation attribute, replace the key with the translated text
        $("input[" + validationAttribute + "],select[" + validationAttribute + "],textarea[" + validationAttribute + "]").each(function (index) {
            $(this).attr(validationAttribute, getLocalizedValue(dictionary, $(this).attr(validationAttribute)));
        });

    }

    function getLocalizedValue(dictionary, key) {

 // Look up the value for the passed key
        for (var item in dictionary) {
            if (dictionary.hasOwnProperty(item)) {
                if (dictionary[item].Key == key) {
                    return dictionary[item].Value;
                }
            }
        }

        return "";

    }