Click here to Skip to main content
15,895,084 members
Articles / Web Development / ASP.NET

A CAPTCHA Server Control for ASP.NET

Rate me:
Please Sign up or sign in to vote.
4.84/5 (166 votes)
31 Jan 20077 min read 2M   32.5K   408  
A CAPTCHA control implemented as a simple, visual drag-and-drop Server Control for ASP.NET.
Imports System.ComponentModel
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Collections
Imports System.Collections.Specialized

''' <summary>
''' CAPTCHA ASP.NET 2.0 user control
''' </summary>
''' <remarks>
''' add a reference to this DLL and add the CaptchaControl to your toolbox;
''' then just drag and drop the control on a web form and set properties on it.
'''
''' Jeff Atwood
''' http://www.codinghorror.com/
''' </remarks>
<DefaultProperty("Text")> _
Public Class CaptchaControl
    Inherits System.Web.UI.WebControls.WebControl
    Implements INamingContainer
    Implements IPostBackDataHandler
    Implements IValidator

    Public Enum Layout
        Horizontal
        Vertical
    End Enum

    Public Enum CacheType
        HttpRuntime
        Session
    End Enum

    Private _timeoutSecondsMax As Integer = 90
    Private _timeoutSecondsMin As Integer = 3
    Private _userValidated As Boolean = True
    Private _text As String = "Enter the code shown:"
    Private _font As String = ""
    Private _captcha As CaptchaImage = New CaptchaImage
    Private _layoutStyle As Layout = Layout.Horizontal
    Private _prevguid As String
    Private _errorMessage As String = ""
    Private _cacheStrategy As CacheType = CacheType.HttpRuntime

#Region "  Public Properties"

    <Browsable(False), _
    Bindable(True), _
    Category("Appearance"), _
    DefaultValue("The text you typed does not match the text in the image."), _
    Description("Message to display in a Validation Summary when the CAPTCHA fails to validate.")> _
    Public Property ErrorMessage() As String Implements System.Web.UI.IValidator.ErrorMessage
        Get
            If Not _userValidated Then
                Return _errorMessage
            Else
                Return ""
            End If
        End Get
        Set(ByVal value As String)
            _errorMessage = value
        End Set
    End Property

    <Browsable(False), _
    Category("Behavior"), _
    DefaultValue(True), _
    Description("Is Valid"), _
    DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)> _
    Public Property IsValid() As Boolean Implements System.Web.UI.IValidator.IsValid
        Get
            Return _userValidated
        End Get
        Set(ByVal value As Boolean)
        End Set
    End Property

    Public Overrides Property Enabled() As Boolean
        Get
            Return MyBase.Enabled
        End Get
        Set(ByVal value As Boolean)
            MyBase.Enabled = value
            ' When a validator is disabled, generally, the intent is not to
            ' make the page invalid for that round trip.
            If Not value Then
                _userValidated = True
            End If
        End Set
    End Property


    <DefaultValue("Enter the code shown above:"), _
    Description("Instructional text displayed next to CAPTCHA image."), _
    Category("Appearance")> _
    Public Property [Text]() As String
        Get
            Return _text
        End Get
        Set(ByVal Value As String)
            _text = Value
        End Set
    End Property

    <DefaultValue(GetType(CaptchaControl.Layout), "Horizontal"), _
    Description("Determines if image and input area are displayed horizontally, or vertically."), _
    Category("Captcha")> _
    Public Property LayoutStyle() As Layout
        Get
            Return _layoutStyle
        End Get
        Set(ByVal Value As Layout)
            _layoutStyle = Value
        End Set
    End Property

    <DefaultValue(GetType(CaptchaControl.CacheType), "HttpRuntime"), _
    Description("Determines if CAPTCHA codes are stored in HttpRuntime (fast, but local to current server) or Session (more portable across web farms)."), _
    Category("Captcha")> _
    Public Property CacheStrategy() As CacheType
        Get
            Return _cacheStrategy
        End Get
        Set(ByVal value As CacheType)
            _cacheStrategy = value
        End Set
    End Property

    <Description("Returns True if the user was CAPTCHA validated after a postback."), _
    Category("Captcha")> _
    Public ReadOnly Property UserValidated() As Boolean
        Get
            Return _userValidated
        End Get
    End Property


    <DefaultValue(""), _
    Description("Font used to render CAPTCHA text. If font name is blank, a random font will be chosen."), _
    Category("Captcha")> _
    Public Property CaptchaFont() As String
        Get
            Return _font
        End Get
        Set(ByVal Value As String)
            _font = Value
            _captcha.Font = _font
        End Set
    End Property

    <DefaultValue(""), _
    Description("Characters used to render CAPTCHA text. A character will be picked randomly from the string."), _
    Category("Captcha")> _
    Public Property CaptchaChars() As String
        Get
            Return _captcha.TextChars
        End Get
        Set(ByVal Value As String)
            _captcha.TextChars = Value
        End Set
    End Property

    <DefaultValue(5), _
    Description("Number of CaptchaChars used in the CAPTCHA text"), _
    Category("Captcha")> _
    Public Property CaptchaLength() As Integer
        Get
            Return _captcha.TextLength
        End Get
        Set(ByVal Value As Integer)
            _captcha.TextLength = Value
        End Set
    End Property

    <DefaultValue(2), _
    Description("Minimum number of seconds CAPTCHA must be displayed before it is valid. If you're too fast, you must be a robot. Set to zero to disable."), _
    Category("Captcha")> _
    Public Property CaptchaMinTimeout() As Integer
        Get
            Return _timeoutSecondsMin
        End Get
        Set(ByVal Value As Integer)
            If Value > 15 Then
                Throw New ArgumentOutOfRangeException("CaptchaTimeout", "Timeout must be less than 15 seconds. Humans aren't that slow!")
            End If
            _timeoutSecondsMin = Value
        End Set
    End Property

    <DefaultValue(90), _
    Description("Maximum number of seconds CAPTCHA will be cached and valid. If you're too slow, you may be a CAPTCHA hack attempt. Set to zero to disable."), _
    Category("Captcha")> _
    Public Property CaptchaMaxTimeout() As Integer
        Get
            Return _timeoutSecondsMax
        End Get
        Set(ByVal Value As Integer)
            If Value < 15 And Value <> 0 Then
                Throw New ArgumentOutOfRangeException("CaptchaTimeout", "Timeout must be greater than 15 seconds. Humans can't type that fast!")
            End If
            _timeoutSecondsMax = Value
        End Set
    End Property

    <DefaultValue(50), _
    Description("Height of generated CAPTCHA image."), _
    Category("Captcha")> _
    Public Property CaptchaHeight() As Integer
        Get
            Return _captcha.Height
        End Get
        Set(ByVal Value As Integer)
            _captcha.Height = Value
        End Set
    End Property

    <DefaultValue(180), _
    Description("Width of generated CAPTCHA image."), _
    Category("Captcha")> _
    Public Property CaptchaWidth() As Integer
        Get
            Return _captcha.Width
        End Get
        Set(ByVal Value As Integer)
            _captcha.Width = Value
        End Set
    End Property

    <DefaultValue(GetType(CaptchaImage.FontWarpFactor), "Low"), _
    Description("Amount of random font warping used on the CAPTCHA text"), _
    Category("Captcha")> _
    Public Property CaptchaFontWarping() As CaptchaImage.FontWarpFactor
        Get
            Return _captcha.FontWarp
        End Get
        Set(ByVal Value As CaptchaImage.FontWarpFactor)
            _captcha.FontWarp = Value
        End Set
    End Property

    <DefaultValue(GetType(CaptchaImage.BackgroundNoiseLevel), "Low"), _
    Description("Amount of background noise to generate in the CAPTCHA image"), _
    Category("Captcha")> _
    Public Property CaptchaBackgroundNoise() As CaptchaImage.BackgroundNoiseLevel
        Get
            Return _captcha.BackgroundNoise
        End Get
        Set(ByVal Value As CaptchaImage.BackgroundNoiseLevel)
            _captcha.BackgroundNoise = Value
        End Set
    End Property

    <DefaultValue(GetType(CaptchaImage.LineNoiseLevel), "None"), _
    Description("Add line noise to the CAPTCHA image"), _
    Category("Captcha")> _
    Public Property CaptchaLineNoise() As CaptchaImage.LineNoiseLevel
        Get
            Return _captcha.LineNoise
        End Get
        Set(ByVal Value As CaptchaImage.LineNoiseLevel)
            _captcha.LineNoise = Value
        End Set
    End Property
#End Region

    Public Sub Validate() Implements System.Web.UI.IValidator.Validate
        '-- a no-op, since we validate in LoadPostData
    End Sub

    Private Function GetCachedCaptcha(ByVal guid As String) As CaptchaImage
        If _cacheStrategy = CacheType.HttpRuntime Then
            Return CType(HttpRuntime.Cache.Get(guid), CaptchaImage)
        Else
            Return CType(HttpContext.Current.Session.Item(guid), CaptchaImage)
        End If
    End Function

    Private Sub RemoveCachedCaptcha(ByVal guid As String)
        If _cacheStrategy = CacheType.HttpRuntime Then
            HttpRuntime.Cache.Remove(guid)
        Else
            HttpContext.Current.Session.Remove(guid)
        End If
    End Sub

    ''' <summary>
    ''' are we in design mode?
    ''' </summary>
    Private ReadOnly Property IsDesignMode() As Boolean
        Get
            Return HttpContext.Current Is Nothing
        End Get
    End Property

    ''' <summary>
    ''' Validate the user's text against the CAPTCHA text
    ''' </summary>
    Private Sub ValidateCaptcha(ByVal userEntry As String)

        If Not Visible Or Not Enabled Then
            _userValidated = True
            Return
        End If

        '-- retrieve the previous captcha from the cache to inspect its properties
        Dim ci As CaptchaImage = GetCachedCaptcha(_prevguid)
        If ci Is Nothing Then
            Me.ErrorMessage = "The code you typed has expired after " & Me.CaptchaMaxTimeout & " seconds."
            _userValidated = False
            Return
        End If

        '--  was it entered too quickly?
        If Me.CaptchaMinTimeout > 0 Then
            If (ci.RenderedAt.AddSeconds(Me.CaptchaMinTimeout) > Now) Then
                _userValidated = False
                Me.ErrorMessage = "Code was typed too quickly. Wait at least " & Me.CaptchaMinTimeout & " seconds."
                RemoveCachedCaptcha(_prevguid)
                Return
            End If
        End If

        If String.Compare(userEntry, ci.Text, True) <> 0 Then
            Me.ErrorMessage = "The code you typed does not match the code in the image."
            _userValidated = False
            RemoveCachedCaptcha(_prevguid)
            Return
        End If

        _userValidated = True
        RemoveCachedCaptcha(_prevguid)
    End Sub

    ''' <summary>
    ''' returns HTML-ized color strings
    ''' </summary>
    Private Function HtmlColor(ByVal color As Drawing.Color) As String
        If color.IsEmpty Then Return ""
        If color.IsNamedColor Then
            Return color.ToKnownColor.ToString
        End If
        If color.IsSystemColor Then
            Return color.ToString
        End If
        Return "#" & color.ToArgb.ToString("x").Substring(2)
    End Function

    ''' <summary>
    ''' returns css "style=" tag for this control
    ''' based on standard control visual properties
    ''' </summary>
    Private Function CssStyle() As String
        Dim sb As New System.Text.StringBuilder
        Dim strColor As String

        With sb
            .Append(" style='")

            If BorderWidth.ToString.Length > 0 Then
                .Append("border-width:")
                .Append(BorderWidth.ToString)
                .Append(";")
            End If
            If BorderStyle <> WebControls.BorderStyle.NotSet Then
                .Append("border-style:")
                .Append(BorderStyle.ToString)
                .Append(";")
            End If
            strColor = HtmlColor(BorderColor)
            If strColor.Length > 0 Then
                .Append("border-color:")
                .Append(strColor)
                .Append(";")
            End If

            strColor = HtmlColor(BackColor)
            If strColor.Length > 0 Then
                .Append("background-color:" & strColor & ";")
            End If

            strColor = HtmlColor(ForeColor)
            If strColor.Length > 0 Then
                .Append("color:" & strColor & ";")
            End If

            If Font.Bold Then
                .Append("font-weight:bold;")
            End If

            If Font.Italic Then
                .Append("font-style:italic;")
            End If

            If Font.Underline Then
                .Append("text-decoration:underline;")
            End If

            If Font.Strikeout Then
                .Append("text-decoration:line-through;")
            End If

            If Font.Overline Then
                .Append("text-decoration:overline;")
            End If

            If Font.Size.ToString.Length > 0 Then
                .Append("font-size:" & Font.Size.ToString & ";")
            End If

            If Font.Names.Length > 0 Then
                Dim strFontFamily As String
                .Append("font-family:")
                For Each strFontFamily In Font.Names
                    .Append(strFontFamily)
                    .Append(",")
                Next
                .Length = .Length - 1
                .Append(";")
            End If

            If Height.ToString <> "" Then
                .Append("height:" & Height.ToString & ";")
            End If
            If Width.ToString <> "" Then
                .Append("width:" & Width.ToString & ";")
            End If

            .Append("'")
        End With
        If sb.ToString = " style=''" Then
            Return ""
        Else
            Return sb.ToString
        End If
    End Function

    ''' <summary>
    ''' render raw control HTML to the page
    ''' </summary>
    Protected Overrides Sub Render(ByVal Output As HtmlTextWriter)
        With Output
            '-- master DIV
            .Write("<div")
            If CssClass <> "" Then
                .Write(" class='" & CssClass & "'")
            End If
            .Write(CssStyle)
            .Write(">")

            '-- image DIV/SPAN
            If Me.LayoutStyle = Layout.Vertical Then
                .Write("<div style='text-align:center;margin:5px;'>")
            Else
                .Write("<span style='margin:5px;float:left;'>")
            End If
            '-- this is the URL that triggers the CaptchaImageHandler
            .Write("<img src=""CaptchaImage.aspx")
            If Not IsDesignMode Then
                .Write("?guid=" & Convert.ToString(_captcha.UniqueId))
            End If
            If Me.CacheStrategy = CacheType.Session Then
                .Write("&s=1")
            End If
            .Write(""" border='0'")
            If ToolTip.Length > 0 Then
                .Write(" alt='" & ToolTip & "'")
            End If
            .Write(" width=" & _captcha.Width)
            .Write(" height=" & _captcha.Height)
            .Write(">")
            If Me.LayoutStyle = Layout.Vertical Then
                .Write("</div>")
            Else
                .Write("</span>")
            End If

            '-- text input and submit button DIV/SPAN
            If Me.LayoutStyle = Layout.Vertical Then
                .Write("<div style='text-align:center;margin:5px;'>")
            Else
                .Write("<span style='margin:5px;float:left;'>")
            End If
            If _text.Length > 0 Then
                .Write(_text)
                .Write("<br>")
            End If
            .Write("<input name=" & UniqueID & " type=text size=")
            .Write(_captcha.TextLength.ToString)
            .Write(" maxlength=")
            .Write(_captcha.TextLength.ToString)
            If AccessKey.Length > 0 Then
                .Write(" accesskey=" & AccessKey)
            End If
            If Not Enabled Then
                .Write(" disabled=""disabled""")
            End If
            If TabIndex > 0 Then
                .Write(" tabindex=" & TabIndex.ToString)
            End If
            .Write(" value=''>")
            If Me.LayoutStyle = Layout.Vertical Then
                .Write("</div>")
            Else
                .Write("</span>")
                .Write("<br clear='all'>")
            End If

            '-- closing tag for master DIV
            .Write("</div>")
        End With
    End Sub

    ''' <summary>
    ''' generate a new captcha and store it in the ASP.NET Cache by unique GUID
    ''' </summary>
    Private Sub GenerateNewCaptcha()
        If Not IsDesignMode Then
            If _cacheStrategy = CacheType.HttpRuntime Then
                HttpRuntime.Cache.Add(_captcha.UniqueId, _captcha, Nothing, _
                    DateTime.Now.AddSeconds(Convert.ToDouble(IIf(Me.CaptchaMaxTimeout = 0, 90, Me.CaptchaMaxTimeout))), _
                    TimeSpan.Zero, Caching.CacheItemPriority.NotRemovable, Nothing)
            Else
                HttpContext.Current.Session.Add(_captcha.UniqueId, _captcha)
            End If
        End If
    End Sub

    ''' <summary>
    ''' Retrieve the user's CAPTCHA input from the posted data
    ''' </summary>
    Public Function LoadPostData(ByVal PostDataKey As String, ByVal Values As NameValueCollection) As Boolean Implements IPostBackDataHandler.LoadPostData
        ValidateCaptcha(Convert.ToString(Values(Me.UniqueID)))
        Return False
    End Function

    Public Sub RaisePostDataChangedEvent() Implements IPostBackDataHandler.RaisePostDataChangedEvent
    End Sub

    Protected Overrides Function SaveControlState() As Object
        Return CType(_captcha.UniqueId, Object)
    End Function

    Protected Overrides Sub LoadControlState(ByVal state As Object)
        If state IsNot Nothing Then
            _prevguid = CType(state, String)
        End If
    End Sub

    Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
        MyBase.OnInit(e)
        Page.RegisterRequiresControlState(Me)
        Page.Validators.Add(Me)

    End Sub

    Protected Overrides Sub OnUnload(ByVal e As System.EventArgs)
        If Not (Page Is Nothing) Then
            Page.Validators.Remove(Me)
        End If
        MyBase.OnUnload(e)
    End Sub

    Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)
        If Me.Visible Then
            GenerateNewCaptcha()
        End If
        MyBase.OnPreRender(e)
    End Sub

End Class

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
My name is Jeff Atwood. I live in Berkeley, CA with my wife, two cats, and far more computers than I care to mention. My first computer was the Texas Instruments TI-99/4a. I've been a Microsoft Windows developer since 1992; primarily in VB. I am particularly interested in best practices and human factors in software development, as represented in my recommended developer reading list. I also have a coding and human factors related blog at www.codinghorror.com.

Comments and Discussions