Asbestos Supply

2008-10-28 ASP.NET MVC Captcha

I was looking for an ASP.NET MVC Captcha control and stumbled upon Nick Berardi's (http://www.coderjournal.com/2008/06/mvc-captcha-for-preview-release-3/).

First of all I'm using IIS7 and had trouble getting the HttpHandler to register properly.  I ended up with the following steps to get it to work:

  1. Add <add verb="GET" path="captcha.ashx" validate="false" type="ManagedFusion.Web.Handlers.CaptchaImageHandler, ManagedFusion.Web.Captcha" /> to <httphandlers> in Web.config
  2. Add <add name="CaptchaImageHandler" verb="GET" path="captcha.ashx" type="ManagedFusion.Web.Handlers.CaptchaImageHandler, ManagedFusion.Web.Captcha" /> to the <handlers> section within <system.webserver> in Web.config
  3. Add routes.IgnoreRoute("{handler}.ashx"); to Global.asax ### So that got it working, but there were a few pieces of functionality I felt were missing: #### 1) I wanted the ability to style the CaptchaTextBox

The site I'm working with is pretty design intensive and I needed a way to inject CSS information into the CaptchaTextBox Html Helper.  I reworked the CaptchaHelper and modified the CaptchaTextBox as well as added some overloads:

public static string CaptchaTextBox(this HtmlHelper helper, string name)
{
    return helper.CaptchaTextBox(name, null);
}

public static string CaptchaTextBox(this HtmlHelper helper, string name, Object htmlAttributes)
{
    return helper.CaptchaTextBox(name, ((IDictionary<string, object>)new RouteValueDictionary(htmlAttributes)));
}

public static string CaptchaTextBox(this HtmlHelper helper, string name, IDictionary<String, Object> htmlAttributes)
{
    ModelState state;             
    TagBuilder builder = new TagBuilder("input");             
    builder.MergeAttributes<string, object>(htmlAttributes);             
    builder.MergeAttribute("type", "text");             
    builder.MergeAttribute("name", name);             
    builder.MergeAttribute("id", name);             
    builder.MergeAttribute("value", "");             
    builder.MergeAttribute("maxlength", ManagedFusion.Web.Controls.CaptchaImage.TextLength.ToString());             
    builder.MergeAttribute("autocomplete", "off");             
    if (helper.ViewData.ModelState.TryGetValue(name, out state) && (state.Errors.Count > 0))
    {
        builder.AddCssClass("input-validation-error");
    }           
    return builder.ToString(TagRenderMode.SelfClosing);         
}

I got the TagBuilder idea from checking out the Reflector on System.Web.MVC.  Pretty cool stuff there.  So now I can use the CaptchaTextBox like so:

<%= Html.CaptchaTextBox("captcha", new { @class = "field" })%>

2) I wanted the CaptchaValidationAttribute to invalidate my Model if the captcha isn't valid

(instead of inject a captchaValid with a value of false into my routedata which is what it does off the shelf)

For this I modified the CaptchaValidationAttribute class.  The first thing I did was make add an ErrorMessage string property.  Then I modified the OnActionExecutingContext method to look like this:

public override void OnActionExecuting(ActionExecutingContext filterContext)         
{
    // get the guid from the post back             
    string guid = filterContext.HttpContext.Request.Form["captcha-guid"];             

    // check for the guid because it is required from the 
    //rest of the opperation             
    if (String.IsNullOrEmpty(guid))            
    {                 
        filterContext.Controller.ViewData.ModelState.AddModelError(Field, ErrorMessage);                 
        return;             
    } 

    // get values             
    CaptchaImage image = CaptchaImage.GetCachedCaptcha(guid);             
    string actualValue = filterContext.HttpContext.Request.Form[Field];             
    string expectedValue = image == null ? String.Empty : image.Text;   

    // removes the captch from cache so it cannot be used again             
    filterContext.HttpContext.Cache.Remove(guid);

    // validate the captch             
    if (String.IsNullOrEmpty(actualValue) || String.IsNullOrEmpty(expectedValue) || !String.Equals(actualValue, expectedValue, StringComparison.OrdinalIgnoreCase))
    {
        filterContext.Controller.ViewData.ModelState.AddModelError(Field, ErrorMessage);
        return;
    }

Now I can use the CaptchaValidationAttribute like this:

[CaptchaValidationAttribute()]
public ActionResult Register(FormCollection form)         
{
    // INCREDIBLY OVER-SIMPLIFIED BUT YOU GET THE IDEA             
    if (!ViewData.ModelState.IsValid)             
        return View();
    }         
}