A Captcha Image generator for FoxPro
October 01, 2006 •
A couple of areas of my FoxPro WebSite – specifically the wiki – are getting plastered with SPAM these days so I’ve been meaning to create some code to handle wiki:CAPTCHA:CAPTCHA: style validation. CAPTCHA basically displays a verification image next to a textbox that has to be filled out to validate the current request. It’s not a foolproof approach for validation and it has some issues with accessibility but it seems to be a common solution to this problem.
There are a quite a few CAPTCHA controls out there for .NET, but in FoxPro this is a little more tricky to do. You can actually use the FFC classes to do the GDI+ code, but I really didn’t want to drag this 400+k library into my project, so tonight I decided to write a small GDI+ routine that does this for me and well, since I already have a small library of GDI+ routines this doesn’t add any overhead to the project.
And it’s actually easier to write this code in C++ than in Fox <g>.
Here’s the rudimentary code that you can compile with Visual Studio and C++:
DWORD WINAPI GetCaptchaImage(WCHAR *lcText,WCHAR* FontName,
int FontSize, WCHAR *lcOutputFile)
{
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
try
{
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
}
catch(...)
{ return 0; }
// *** Start by figuring out size of the image
Bitmap *tBitmap = new Bitmap(1000,400);
Graphics *graphics = new Graphics(tBitmap);
Font *font = new Font(FontName,(REAL) FontSize);
PointF Origin(0,0);
RectF Box;
graphics->MeasureString(lcText,lstrlenW(lcText),font,Origin,&Box);
int lnWidth = int(Box.Width);
int lnHeight = int(Box.Height * 1.02);
// *** Now create the real thing
tBitmap = new Bitmap(lnWidth,lnHeight );
graphics = new Graphics(tBitmap);
// *** DarkGray Text
SolidBrush *Black = new SolidBrush( Color(102,102,102 ) );
HatchBrush *hBrush = new HatchBrush( HatchStyleSmallConfetti,Color(208,208,208),Color(255,255,255) );
graphics->FillRectangle( hBrush,0,0,lnWidth,lnHeight );
graphics->SetSmoothingMode(SmoothingModeAntiAlias);
graphics->SetTextRenderingHint(TextRenderingHintAntiAlias);
//// *** Draw at 1% angle
Matrix *transformMatrix = new Matrix();
transformMatrix->Rotate(1.0f);
graphics->SetTransform(transformMatrix);
// *** Keep resizing until it fits
while (Box.Width < lnWidth - 10)
{
FontSize++;
delete font;
font = new Font(FontName,(REAL) FontSize);
graphics->MeasureString(lcText,lstrlenW(lcText),font,Origin,&Box);
}
while (Box.Width >= lnWidth)
{
FontSize--;
delete font;
font = new Font(FontName,(REAL) FontSize);
graphics->MeasureString(lcText,lstrlenW(lcText),font,Origin,&Box);
}
graphics->DrawString(lcText,lstrlenW(lcText)+1,font,Origin,Black);
bool Error = false;
try
{
DeleteFile(lcOutputFile); // must delete file or size may not be updated
CLSID loEncoderId;
if (GetEncoderClsid(lcOutputFile,&loEncoderId) == -1)
{
GdiplusShutdown(gdiplusToken);
return 0;
}
tBitmap->Save(lcOutputFile,&loEncoderId,NULL);
}
catch(...)
{
Error = true;
}
delete graphics;
delete tBitmap;
GdiplusShutdown(gdiplusToken);
if (Error)
return 0;
return 1;
}
There’s one dependency function GetEncoderClsId which translates the file extension into a ClsId that GDI+ needs to figure out which encoder to use to save a file. You can find that function in the source code provided.
Now this code is pretty basic – it doesn’t do fancy text warping or randomizing the backgrounds etc. For the moment I’m not too worried about SPAMMERS on my site actually hacking the image generated. If it gets to that point adding additional checks might be worthwhile.
My C++ skills are fast fading. It took me a couple of hours to write this code, primarily because I ran into a problem witht GdiplusShutdown crashing Visual FoxPro. As it turns out if you create hard instances of any GDI+ objects – rather than reference objects created with the new operator – the object stick around before the method is done processing. This in turn causes GdiplusShutdown fire an exception that gets passed back to VFP. It took a while of tweaking to figure this one out.
You can see the images in the Wiki here and in the WebLog here.
You can download the DLL, Fox wrappers and the VC project from here.
Integration in Web Connection
I’ve also integrated this into Web Connection as part of the wwImaging.dll and the wwAPI function class. There’s a new function called GetCaptureImage() that looks like this:
lcText = RIGHT(SYS(2015),5) && Random number
lcFileName = SYS(2003) + "\" + lcText + ".jpg"
GetCaptchaImage(lcText,lcFilename,"Verdana",30)
CAPTCHA makes sense only in Web Applications so here’s a more complete example that uses Captcha to check for user validity in a typical Web Connection Process method:
************************************************************************
* wwAPI :: GetCaptchaImage
****************************************
*** Function: Returns an image for the given text
*** Assume:
*** Pass:
*** Return:
************************************************************************
FUNCTION GetCaptchaImage(lcText,lcOutputFile,lcFont,lnFontSize)
IF EMPTY(lcFont)
lcFont = "Arial"
ENDIF
IF EMPTY(lnFontSize)
lnFontSize = 28
ENDIF
DECLARE INTEGER GetCaptchaImage ;
IN wwImaging.dll as _GetCaptchaImage ;
STRING Text, STRING FONTNAME, integer FontSize, STRING lcOutputFile
lcText = STRCONV(lcText,5) + CHR(0)
lcFont = STRCONV(lcFont,5) + CHR(0)
lcOutputFile = STRCONV(lcOutputFile,5) + CHR(0)
RETURN IIF( _GetCaptchaImage(lcText,lcFont,lnFontSize,lcOutputFile) = 1,;
.T.,.F.)
ENDFUNC
* wwAPI :: GetCaptchaImage
In addition there’s a new default Process method that can automatically serve a CAPTCHA image. You basically submit the CAPTCHA
Here’s what this looks like in a Process method which demonstrates the logic of the process best:
FUNCTION CaptchaTest
lcResult = ""
IF Request.IsPostBack()
*** Captcha Value is stored in a fixed SessionVar
lcCaptcha = Session.GetSessionVar("__CAPTCHA")
IF LOWER(lcCaptcha) == LOWER(Request.Form("txtCaptcha"))
lcResult = "You matched it!"
ELSE
lcResult = "Bing: Try again sucker"
ENDIF
ENDIF
*** Generate a new CAPTCHA AFTER you've checked it!
*** CaptchaImage is a new Process method that handles
*** both ‘submitting’ of the CAPTCHA as well as serving it
Process.CaptchaImage(RIGHT(SYS(2015),5),"Verdana",30)
TEXT TO lcHTML TEXTMERGE NOSHOW
<form method="POST" action="">
Here it is: <img src='CaptchaImage.wwd'> <input name="txtCaptcha" >
<input type="submit" name="btnSubmit" value="save">
</form>
<<lcResult>>
ENDTEXT
this.StandardPage("Output",lcHtml)
ENDFUNC
That’s pretty straight forward, however, it’s even easier with the Web Control Framework. I added a new wwWebCapture control that can be dropped onto any WCF style form like this:
<ww:wwWebCaptcha runat="server" id="Captcha" CaptchaChars="ABCDEF0123456789"
CaptchaLength="5" FontName="Tahoma" FontSize="24" ></ww:wwWebCaptcha>
Then in the CodeBehind code:
FUNCTION btnPostComment_Click()
this.oComment.New()
IF THIS.BindingErrors.Count > 0
this.ErrorDisplay.Text = this.BindingErrors.ToHtml()
RETURN
ENDIF
IF !this.Captcha.IsValidated
this.ErrorDisplay.ShowError("Invalid Validation Code")
this.SetFocus(this.txtComment)
RETURN
ENDIF
…
ENDFUNC
The control manages all the Session settings, the image url creation so there’s really only a single line of code required to check whether the Captcha was validated. That’s it.
Captcha this, Captcha that
Captcha isn’t perfect – it can be hacked and this implementation is as minimal as it can be. There’s no text warping, no variable angles and fonts etc. to make it a little harder to crack. But the reality is that most spamming won’t go through the lengths to create even the simplest of CAPTCHAs – it’s just not worth the effort. If you find that a CAPTCHA gets cracked an relatively easy way to throw it off again is to rename some of your form vars.
Another preventative measure might be to add an alert() box to your form’s onsubmit() functionality. I think most of the spamming is done through some sort of browser automation rather than through HTTP clients and adding an alert() box stops a robot dead. Couple that with something that requires JavaScript to handle none browser clients and you’re reducing the amount of access considerably. There will always be those that actually manually spam – and that’s much, much harder to fight. But labor is cheap in China <s>… (no offense to any far eastern folks, but 90% of the spam I’ve seen over the last few years comes from China.
Also remember Captcha is not accessible. If you have a vision impaired user they may not be able to get past a capture because there’s no text that can ‘be read’ out by the various accessibility features.
Cesar Chalom
December 06, 2006