Judging from the comments section (lack there of), I am certain that no one has noticed my recent bombardment of comment spam. I've been fighting it by deleting rows from the database until I had time to implement some type of solution to combat the spam. Today, I finally made the time to develop a captcha implementation for the site. It is not fully complete as I want to add in support for disabled javascript; however, I thought I would share the solution anyways.
As you may have gathered, I am an avid supporter of dependency injection. What follows is a simple interface I wrote for the captcha service:
public interface ICaptchaService
{
void Initialize(Page page);
bool Validate(Page page);
}
As you may notice, each method takes an instance of the page. I wrote it this way because I wanted to have an unobtrusive solution that would not require any extra work - regardless of where it is implemented. Since I am not a fan of viewstate, I decided to implement a session-based solution (which can easily be swapped out) to handle the captcha. I borrowed and rewrote most of the code from Mads at
this article. Here is my session based implementation:
public class SessionCaptchaService : ICaptchaService
{
private const string CaptchaValueKey = "Captcha.Value";
private const string CaptchaSciptKey = "Captcha.Script";
private const string SetCaptchaScriptKey = "Captcha.Set.Script";
private const string CaptchaFieldKey = "Variable.Field";
public void Initialize(Page page)
{
var session = GetHttpContextSession();
var captchaValue = session[CaptchaValueKey];
if (captchaValue == null)
{
captchaValue = Guid.NewGuid().ToString();
session[CaptchaValueKey] = captchaValue;
}
var script = new StringBuilder();
script.AppendLine("function setVariable(){");
script.AppendFormat("var form = document.getElementById('{0}');", page.Form.ClientID);
script.AppendLine("var variable = document.createElement('input');");
script.AppendLine("variable.type = 'hidden';");
script.AppendFormat("variable.id = '{0}';", CaptchaFieldKey);
script.AppendFormat("variable.name = '{0}';", CaptchaFieldKey);
script.AppendFormat("variable.value = '{0}';", captchaValue);
script.AppendLine("form.appendChild(variable);}");
page.ClientScript.RegisterClientScriptBlock(GetType(), CaptchaSciptKey, script.ToString(), true);
page.ClientScript.RegisterStartupScript(GetType(), SetCaptchaScriptKey, "setVariable();", true);
}
public bool Validate(Page page)
{
var session = GetHttpContextSession();
var captchaValue = session[CaptchaValueKey];
var value = page.Request[CaptchaFieldKey];
if (string.IsNullOrEmpty(value))
{
return false;
}
return value.Equals(captchaValue);
}
private static HttpSessionState GetHttpContextSession()
{
var context = HttpContext.Current;
if (context == null)
{
throw new Exception("This method relies on HttpContext which is null in this instance.");
}
if (context.Session == null)
{
throw new Exception("Session must be enabled for a session-based captcha.");
}
return context.Session;
}
}
This class should be pretty self-explanatory; it receives an instance of the page from the calling code and then runs javascript to add a hidden field to the form that holds a Guid as the value. When the validate method is called on the service, it compares the Guid in session to the form field's Guid to ensure they match.
This solution is definitely far from perfect (but quick to implement) so it will have to do for now. The largest downfalls is that it only works if the spammer has javascript disabled - which will also prevent viewers with javascript turned off from commenting on the site.
Here is the call to the Captcha service (it works from a usercontrol, page, master page, etc.):
protected override void OnLoad(EventArgs e)
{
_captchaService.Initialize(Page);
base.OnLoad(e);
}
protected void uxSubmitButton_Click(object sender, EventArgs e)
{
if (!_captchaService.Validate(Page))
{
return;
}
SaveComment();
}
Hopefully, this will help someone else. I will post again after I make the captcha more robust.