ColdFusion TIPS PLUS
Issue 00138 http://www.cftipsplus.com
I. My CommentsII. ColdFusion In Context: Better Server-Side Validation
By R. Martin Ladner
martin.ladner@charter.net
I. Comments:
Still no cable at home.... What can you do when you can not get DSL and you can not seem to get the cable guy to MOVE IT. I want to get BACK online at HOME.
Oh, my wife is on bed rest and I could work from home and help her if I was HOME...
Keep Coding,
Nathan Stanford
http://www.cftipsplus.com
If you have suggestions for articles send them to us.
If you would like to write for cftipsplus.com
send us an email to:
NathanS<at>nsnd.com
IF YOU WANT TO BE AN AUTHOR SEND IN YOUR COLDFUSION TIPS.
Remember this is a great way to get your name known in the
ColdFusion Community.
II. ColdFusion in Context: Better Server-Side Validation
By R. Martin Ladner
martin.ladner@charter.net
Routine error messages from the server side are getting more attention lately as the U.S. Government uses Section 508 to put weight behind the movement to make and keep the Web usable to individuals who can't use a mouse and whose talking "browsers" might not read javascript. The ability to readily produce server-side error messages is valuable in providing convenience to customers.
One of the capabilities of ColdFusion is automatic server-side validation. This is triggered by having extra form fields specify the validation that the "real" fields should have. For example, if you have a field named Username and a hidden field whose name is Username_required, ColdFusion will require the Username field to have something in it.
However, there are three problems with this approach as it's implemented today. You must add a hidden field for each requirement to be met. You generally want to customize only the nice field label but must instead specify the entire message or take a default message that includes the raw field name. And, the built-in approach cannot be extended to provide additional kinds of custom validation or provide existing kinds more smoothly.
This tip proposes a better way to add server-side validation. Not only is it faster to apply and easier to extend than the native server-side method, but it's also faster to apply and easier to extend than the native client-side method.
Ideas for Convenience
Let's start with the hidden field idea again, but this time, let's start with the proposition that we want to do as little extra typing as possible. Let's also agree that we're using this tool for convenience, not to block determined hackers. (We'll talk about more potent but less flexible tools later.) Our intention for now is to tell the user which fields weren't entered right and what it will take to fix them.
Suppose you could specify multiple tests with a single hidden field and only had to specify the data label instead of detailed instructions to the user. That's what this alternative will demonstrate with just one hidden field per data field, no matter how many requirements you want it to meet.
You'll need to specify three pieces of information in that hidden field. Because you can't guarantee the sequence in which the form fields reach the server, you'll need a sequence. You'll need a list of requirements to meet. And, you'll need the data label to become part of any error messages. Let's separate them with a tilde (~) character.
So the program will know which data to apply this requirement to, give the hidden field the same name as the data field, only add a suffix of "_t" to the name of the hidden field. Notice that the suffix is the same no matter how many requirements or which requirements the data field must satisfy.
You'll specify the field sequence with numbers less than one. This makes it easy to insert fields later on without renumbering.
For the proof of concept, assume you have written five canned tests: z (zero length), p (plain text), l (letters only), d (digits only), and s (spaces). You'll use any combination of these that makes sense to you.
For the third piece of information, you'll specify the data label, not instructions to the user.
Put this sample form in MyForm.cfm.
<form action="MyAction.cfm" method="post">
Enter the following to see automatic server-side validation
and formatting...<br>
Enter a Nickname without problem characters:
<input type="text" name="MyName"><br>
<input type="hidden" name="MyName_t"
value=".1~pz~Nickname">
You may enter a Your Digit String:
<input type="text" name="MyDigits"><br>
<input type="hidden" name="MyDigits_t"
value=".18~d~Your Digit String">
Enter Your Letter String:
<input type="text" name="MyLetters"><br>
<input type="hidden" name="MyLetters_t"
value=".25~lz~Your Letter String">
Enter Running Text without spaces:
<input type="text" name="Mouth"><br>
<input type="hidden" name="Mouth_t"
value=".3~sz~Running Text">
<input type="submit" name="doit" value="Go">
</form>
Put this code in MyAction.cfm. Notice that you don't have to list the fields over again in any way. The program flow won't make it past Autoval.cfm if fields are wrong.
<cfinclude template="ServerVal.cfm">
Congratulations on successful completion of your entry!
Put the rest of the code in ServerVal.cfm. It will 1) pull the test information into a list, 2) give the sequence numbers a common format, 3) sort the list by sequence number, 4) Run requested tests to build the error message line by line, and 5) if an error was found, display the error message and stop the process flow.
To pull the test information into a list, define a line separator (|), an element separator (~), use structKeyList() to walk through the form field names, and add those names to a list of test requirements. You could use different characters other than a pipe and a tilde. Just be consistent. If you change the tilde here, change it in your forms as well.
<!--- Pull the test information into a list:
sequence~requirement~label~fieldname --->
<cfset TestList="">
<cfset LS="|">
<cfset ES="~">
<cfloop list="#structKeyList(form)#" index="Key">
<cfif Key contains "_t">
<cfset TestList=TestList&LS&form[Key]&ES&Key>
</cfif>
</cfloop>
Because the sequence number will receive a text sort, it's not enough to use it in its raw form. To force the sequence numbers to a common format, you need to walk through the list line by line and then replace the first item on each line with a formatted version of itself.
<!--- Give the sequence numbers a common format --->
<cfset Nr=1>
<cfloop list="#TestList#" index="Line" delimiters="#LS#">
<cfset skip code when desired --->
<cfloop from=1 to=1 index="Pass">
<!--- If empty, skip all tests but the zero test --->
<cfif not len(trim(DataValue))>
<!--- Zero test --->
<cfif Requirement contains "z">
<cfset ErrorMsg=ErrorMsg&LS&Label&
" must not be empty">
</cfif>
<cfbreak>
</cfif>
Regular expressions are your friend when it comes to specifying formats. Double pound signs and double your double quotes. Put a backslash in front of other special characters. The plus sign after the square brackets means "one or more times". If one or more of these characters is found, the code complains.
<!--- Plain test --->
<cfif Requirement contains "p">
<cfif reFind("[%##\>\*\""\<~]+",DataValue)>
<cfset ErrorMsg=
ErrorMsg&LS&Label&' cannot contain %, ##, >, *, ", <, or ~.'>
</cfif>
</cfif>"
After the plain test, the spaces test is easy.
<!--- Spaces test --->
<cfif Requirement contains "s">
<cfif reFind("[ ]+",DataValue)>
<cfset ErrorMsg=
ErrorMsg&LS&Label&" cannot contain spaces">
</cfif>
</cfif>
The digits test introduces a new wrinkle. To find items that are NOT in the square brackets, make an up caret (^) the first character inside them. Regular expressions understand ranges. So, the digit test says that if you see one or more characters that is not in the range of 0-9, complain.
<!--- Digits test --->
<cfif Requirement contains "d">
<cfif reFind("[^0-9]+",DataValue)>
<cfset ErrorMsg=
ErrorMsg&LS&Label&" can contain only digits">
</cfif>
</cfif>
The letters test makes the same complaint for characters that aren't from a-z. However, by using the reFindNoCase, it will treat A-Z the same way that it treats a-z.
<!--- Letters test --->
<cfif Requirement contains "l">
<cfif reFindNoCase("[^a-z]+",DataValue)>
<cfset ErrorMsg=
ErrorMsg&LS&Label&" can contain only letters">
</cfif>
</cfif>
Wind down. End the one-pass loop. Continue the main loop until all requirement fields have been checked. If the error message isn't empty, loop through it, tell the user how to get back to the previous page, and use the cfabort tag to stop processing. If the error message is empty, processing will continue with the rest of whatever tag included this one.
<!--- End one-pass loop --->
</cfloop>
<!--- Test the next field --->
</cfloop>
<cfif len(ErrorMsg)>
<cfloop list="#ErrorMsg#" index="Line" delimiters="#LS#">
<cfoutput>#Line#</cfoutput><br>
</cfloop>
Press your back button, correct any problems, and re-submit.
<cfabort>
</cfif>
Run the Demo
Browse MyForm.cfm, filling in the fields in various ways, until you get tired of error messages. The error page shows everything that's wrong, not just the first problem it encounters in the form.
Considerations for Use
Suppose you haven't been validating your database inputs on the server side until now. You've counted on javascript to block bad input at the client. If this has been the case and you're belatedly finding that your site has users whose talking browsers don't read javascript alerts, it's time to step up to the challenge. You can let those users get raw database errors, or you can polish this technique. Imagine not having to write custom code for every field. This could get to be easy.
Stronger Methods
Remember that this method depends on users who aren't deliberately dropping fields from the form. For those few situations where you need to step beyond convenience to fiscal control (situations usually involving required fields), use something the user can't just delete.
You could place a special "required" ending on the data field names themselves instead of using a separate hidden field. If the user tampered with the data field name, the data would not reach its destination.
You could encode business rules into the database. Users who try to hack the system don't need nice-looking error messages as long as an error is thrown.
However, for your routine needs, the technique demonstrated here is simple to use and easy to extend.
=Marty=
Publisher and Creator:
Nathan Stanford,
NathanS<at>nsnd.com
http://www.cftipsplus.com
Macromedia and ColdFusion are U.S. registered trademarks.
Copyright (c) 2000 - 2003
CFTIPSPLUS.COM and NSND.COM
Permission is granted to circulate this publication via
MANUAL forwarding by email to friends provided that the text is
forwarded in its entirety and no fee is charged.