Preventing Spam With An ASP.NET CAPTCHA Control

The source and dll for the project can be downloaded here: CaptchaControl.zip About A CAPTCHA image displays text that is readable to humans, but not to computers. The concept is useful because it provides a means to filter out automated programs while allowing real users to pass through. Bots for example, which spam comments on weblogs and other dynamic websites, are a fairly pervasive problem and even a regularly new and low traffic blog like this was getting dozens of spam comments every day. CAPTCHA is a means to stop them while still allowing real users to post.CaptchaControl is an ASP.NET server control that is designed to start working by just being dropping onto the page, without any modification. This version is based off Michael Trefry’s implementation, which is in turn based off Dan Burke’s. My update fixes a couple of issues around storing the code in a browser cookie and adds features like client validation. CatchaControl OverviewThe CaptchaControl class is a composite control and encapsulates the CAPTCHA image, a textbox for capturing the result and a validator for verifying the correct code was entered. It handles hooking all the controls together, pointing the image src towards the image handler and maintaining the state of the code, which is stored encrypted in a hidden form field and then decrypted on postback.

protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);
 
    RegenerateCode();
 
    Page.ClientScript.RegisterHiddenField(ClientID + "__hidden", _encryptedCode);
    Page.RegisterRequiresPostBack(this);
 
    SetChildProperties();
}
 
bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection)
{
    _encryptedCode = postCollection[ClientID + "__hidden"];
    _code = Encryptor.Decrypt(_encryptedCode, GetKey(), GetIV());
 
    return true;
}

CaptchaControl has a number of properties for modifying its behavior such as the text of the error message, whether to display for authenticated users and whether the code should be validated on the client with JavaScript (note that the unencrypted code will be included with the page if this is enabled). It also an optional LayoutTemplate property of ITemplate that allows the encapsulated controls layout to be customized.Generating the CAPTCHA ImageThe CAPTCHA image is generated by CaptchaHandler, a HttpModule. Parameters in the querystring, including the encrypted code data, are passed to a class than handles generating the image, which is then rendered to the response.

void IHttpHandler.ProcessRequest(HttpContext context)
{
    HttpRequest request = context.Request;
    HttpResponse response = context.Response;
 
    string encryptedCode = request.QueryString["code"];
    string code = Encryptor.Decrypt(encryptedCode, CaptchaControl.GetKey(), CaptchaControl.GetIV());
 
    int width = Convert.ToInt32(request.QueryString["width"]);
    int height = Convert.ToInt32(request.QueryString["height"]);
 
    CaptchaImageGenerator captchaImage = new CaptchaImageGenerator(code, width, height);
 
    response.Clear();
    response.ContentType = "img/jpeg";
    response.Cache.SetCacheability(HttpCacheability.NoCache);
 
    captchaImage.RenderImage(response.OutputStream);
}

UsageRegister the CaptchaHandler by adding the following to your web.config’s httpModule element.

<addverb="*"path="captchahandler.axd"type="Newtonsoft.CaptchaControl.CaptchaHandler, Newtonsoft.CaptchaControl"/>

To add the CaptchaControl to a page simply place it where you want it to be displayed:

<%@ Register TagPrefix="ncc" Namespace="Newtonsoft.CaptchaControl" Assembly="Newtonsoft.CaptchaControl" %>
 
<ncc:CaptchaControl runat="server" ID="CaptchaControl" />

The IsValid property on the page will automatically be set when the page’s validators, including CaptchaControl, are evaluated on postback.

private void btnSubmit_Click(object sender, EventArgs e)
{
    if (IsValid)
    {
        // logic
    }
}

The important question: Does it work? Since adding CAPTCHA validation to this blog comment spam has dropped from dozens each day to zero.The source and dll for the project can be downloaded here: CaptchaControl.zip

Logging JavaScript Errors To ASP.NET

Over the past few years a huge jump in the complexity of JavaScript has occured on the browser, resulting in richer, more responsive and user friendly web applications. Unfortunatly this shift does not come without cost.Greater complexity on the client means more room for bugs, and without some effective means to log them you are left in the dark about problems until users are driven to complain.Solution OverviewThe client exception logger comprises of three parts: a script on the client that catches the exceptions and sends them to the server, a HttpHandler on the server that receives the error details and logs them, and an ASP.NET control that links the two together.Details logged include the following:

  • JavaScript exception message, line and file name
  • Date and time
  • IP Address
  • User agent (browser and platform details)
  • Session ID

Catching and Sending the Error on the Client Whenever an exception is not caught in JavaScript it triggers an onerror event on the browser's window object. It is comparable to ASP.NET’s Application_Error event in the global.asax.When the page is downloaded a ClientLogger object is created and a function is attached to the onerror event. That function calls a method on ClientLogger object which will send the error details to ASP.NET on the server using the XMLHttpRequest object. This is done asynchronously and does not effect the user.

var logger = new ClientLogger({handlerUrl:'../../ClientLogger.axd',sessionID:'2rnbawbkzuovsw55ymvflz45'});
window.onerror = function(message, url, line) { logger.Log(message, url, line); };

Receiving and Logging the Error on the Server When sending the error data back to the server, rather than the page calling back to itself, the logger posts the details a custom HttpHandler. This is done to minimize side effects or performance issues of creating the page again.Note that the handler does not contain any logic for logging the error details. Instead it uses the new ASP.NET 2.0 Web Event feature, and raises a custom WebJavaScriptErrorEvent event.

WebBaseEvent.Raise(new WebJavaScriptErrorEvent(jsMessage, this, jsUrl, jsLine, pageUrl, userAgent, sessionID));

Packaging Everything Together in a Control The ASP.NET control includes the JavaScript file (embedded in the dll) on the page and registers some JavaScript that creates the client object and attaches it to the onerror event. To enable JavaScript error logging include the control on your master page:

<%@ Register TagPrefix="ncl" Namespace="Newtonsoft.ClientLogger" Assembly="Newtonsoft.ClientLogger" %>
<ncl:ClientLogger runat="server" />

And register the HttpHandler in the web.config:

<addverb="*"path="clientlogger.axd"type="Newtonsoft.ClientLogger.ClientLoggerHandler, Newtonsoft.ClientLogger"/>

Consuming The Web EventThe great thing about ASP.NET Web Events is that you can decide how you want to log the event, and it is simple as modifying your web application's web.config file. For example in just a few lines you can configure ASP.NET to log all errors to your database or send them to an email address. The rule below for example will log all errors to the database. You can learn more about using ASP.NET Web Events here.

<addname="SqlServerWebErrorLogger"
    eventName="All Errors"
    provider="SqlWebEventProvider"/>

And that's it. You will now know about JavaScript errors as they happen.The source and dll for the project can be downloaded here: ClientLogger.zip

WebDialog 1.0.4 released

WebDialog 1.0.4 has been released.

The main new feature in this release is the state of the dialog (visibility, size and position) being remembered on post back. Also new is preliminary support for working within Atlas' UpdatePanel control. Note that this support won't effect regular ASP.NET 2.0 websites.

What's New:

  • New Feature - Visibility, size and position state posted back from client.
  • New Feature - Preliminary Atlas support.

Testing if a ContentPlaceHolder is Empty

K. Scott Allen, one of the many Scotts who seem to develop in .NET, has written a comprehensive article called Master Pages: Tips, Tricks, and Traps that looks at the new ASP.NET 2.0 feature. I use a couple of those features myself to handle page titles and descriptions, and I highly recommend it to anyone planning to use master pages in the near feature.One trick that I’ve found useful and that isn't in the article is how to modify master page content depending upon whether a ContentPlaceHolder has any content.For example you might have a ContentPlaceHolder on your master for related pages. To make it easier to later modify your design later you want to put the HTML for the container and title in the master, rather than every individual page.

<div id="divRelatedPages" runat="server">
    <h3>Related Pages...</h3>
    <asp:ContentPlaceHolder ID="cphRelatedPages" runat="server" />
</div>

Unfortunately doing it this way will leave a box and title without anything else on a page that didn’t include any related links. The fix to this problem is testing if the ContentPlaceHolder has any content, hiding the divRelatedPages control and title if it doesn’t.To make this test easy I’ve created an IsControlEmpty method. The code inside it is quite simple. It returns true if the placeholder has no child controls or if it has just one child LiteralControl that contains nothing that whitespace. A blank LiteralControl is what is created when an auto-generated placeholder is left empty.

public static bool IsControlEmpty(Control control)
{
    if (control == null)
        throw new ArgumentNullException("control");
 
    ControlCollection controls = control.Controls;
 
    // control has no children
    if (controls.Count == 0)
    {
        return true;
    }
    else if (controls.Count == 1)
    {
        LiteralControl staticContent = controls[0] as LiteralControl;
 
        // control only has a literal child which is either empty,
        // or contains nothing but whitespace
        if (staticContent != null)
        {
            if (string.IsNullOrEmpty(staticContent.Text))
                return true;
            else if (StringUtils.IsWhiteSpace(staticContent.Text))
                return true;
        }
    }
 
    return false;
}
 
public static bool IsWhiteSpace(string s)
{
    for (int i = 0; i < s.Length; i++)
    {
        if (!char.IsWhiteSpace(s[i]))
            return false;
    }
    return true;
}

Now simply pass the placeholder control to the method and hide divRelatedPages based on the result.

if (ControlUtils.IsControlEmpty(cphRelatedPages))
    divRelatedPages.Visible = false;