Last week at the Boston CFUG meeting, we had an "open mic" night where CFUG members went up and gave short presentations in front of the group. I decided to follow up the talk that Luis Majano gave on the ColdBox framework back in July and give an intro on ColdBox plugins. I had some technical problems with my Winblows laptop early on, but Brian Rinaldi graciously let me borrow his laptop for my presentation. It was fun to present and it went well overall. But due to time constraints, I didn't get a chance to go over the code of the custom plugin that I demoed, so I decided to do it here on my blog. (Besides, it's been getting stale around here).
I built a plugin to display CAPTCHA images and validate form input against them using CF8's cfimage tag. It was pretty simple to do. To prepare, I looked at the code for another visual ColdBox plugin, the Messagebox plugin, the ColdBox docs, and this blog post by Ray Camden on CAPTCHA with cfimage. The plugin can be downloaded by clicking the link at the bottom of this post. Let's take a look at the code to see how it works.
This is the general format for the cfcomponent tag and the init() method of ColdBox plugins. The only think noteworthy about the cfcomponent tag is that I'm telling the framework to cache this plugin by adding the metadata attribute cache="true". By default, ColdBox plugins are not cached. But since this plugin us generally used over multiple requests (for displaying and then validating the CAPTCHA), I decided to have it cached. By default it uses the caching parameters in the framework's config files, but these can be overriden if desired by using the cachetimeout and cacheLastAccessTimeout attributes (see the docs for more info). Also, all plugins must extend the base plugin class (coldbox.system.plugin).
The init() function just takes the coldbox controller as an argument and sets some basic info about the plugin (name, version, description). The beauty of plugins, though, is that you don't need to worry about the init() method when you call them because the framework runs the initualization for you.
Let's take a look at the rest of the code.
The ony public methods of this plugin are display() and validate(). The display() method is essentially a wrapper for the cfimage action="captcha" tag. It's set up with default arguments so that calling "display()" will give you a 50 X 200 pixel captcha image of 3 to 5 random characters (the length will range between +/- 1 of the value in the "length" argument). Of course, you can customize the image by passing in arguments. The default random characters are built using the makeRandomString() function which was taken from Ray's blog post. But if you have your own way of generating the captcha text, you can pass that text in as an argument (in which case the "length" argument will be ignored). The "message" argument sets an error message which will appear if under the CAPTCHA image if the user is redirected back to the form after a failed CAPTCHA validation. This message can be be styled using the "cb_captchamessage" css class to make it stand out
Before the display() method sets up the image, it calls the setCaptchaCode() function. What this does is set the CAPTCHA text and a "validated" flag in a structure inside the session scope. That structure (cb_captcha) is accessed through the getCaptchaStorage() function. OOP purists out there will point out that it's not good practice to directly reference shared scopes (like the session scope) in your cfc's because it breaks encapsulation. Yes, technically, one should use a session facade, but I decided to side on pragmatism here and keep things simple. However, since I did isolate access to the session scope to the getCaptchaStorage() method, I made it very easy for anyone to modify the code to use a facade. In fact, the ColdBox "SessionStorage" plugin can be used for that purpose (since it's possible for plugins to call other plugins). Also note that the CAPTCHA text is converted to lower case before it is hashed and stored. I did this to make the validation case-insensitive. You can simply take the lcase() out to make it case-sensitive or even set up another argument to control case sensitivity.
The validate() method simply takes in a string and validates it against the CAPTCHA code that was set up to display. It then sets the "validated" flag to true or false based on if the values were equal. This can be used to redirect users back to the form or go ahead and process the form.
Usage
So here is an example of how you would use this plugin. First, you would use the display() method in your form view along with an input field for people to enter the code:
Now, in your event handler that would handle this form input, you would do something like this:
That's it! With cfimage CAPTCHA functionality in this tidy little package, you can use CAPTCHA images throughout all of your ColdBox applications. Feel free to download the plugin using the link below, use it, take it apart, and/or modify it to your heart's content.
Also, be sure to check out the two projects that were also presented at our open mic, night: Tom Mollerus's Clickheat for ColdFusion and Charles Kaufmann's Coreforms custom tag library.
Nov 3, 2008 at 10:34 AM Great stuff man!! Added to code depot
Nov 3, 2008 at 11:36 AM Might be cool to have an init() or something on it, where you can specify direct-to-session or use-session-cb-plugin. - WB
Nov 3, 2008 at 1:02 PM @Will Well, there is an init() method already. But the init() method is called by the plugin service when you call getMyPlugin(), so you wouldn't be able to directly pass any arguments to it to specify whether to use the sessionstorage plugin or not. Since the coldbox controller is passed into the init method of all plugins, you have access to the settings in the coldbox config file. So you could set up a setting to control for this and then the init() method could set up the captcha storage struct directly in the session scope or using the sessionstorage plugin, depending on the setting. If I decided to use the sessionstorage plugin, though, I'd just hardcode it into the getCaptchaStorage() method:
<cffunction name="getCaptchaStorage" access="private" returntype="any" output="false">
<cfset var sessionstorage = getPlugin("sessionstorage") />
<cfset var cb_captcha = {captchaCode = "", validated = true} />
<cfif not sessionstorage.exists("cb_captcha")/ > <cfset sessionstorage.setVar("cb_captcha",cb_captcha) />
</cfif>
<cfreturn sessionstorage.getVar("cb_captcha") />
</cffunction>
Nov 3, 2008 at 1:14 PM thanks for the hard work here, works well. :)
Nov 3, 2008 at 2:29 PM My pleasure, Glyn!
Nov 15, 2008 at 6:18 PM Thanks for this plugin Tony, incredibly useful and soooooo easy to integrate. I just added it to Codex Wiki and it took me about 3 minutes!!