ColdFusion in Context: Universal Server-Side Check

Suppose you want to write just one set of programs to do standard server-side validation of every form you will write. Here's how to do that in a manner that re-uses information that supports universal client-side checks. You'll also see how to invoke custom code for unique situations.

Problem and Solution

For each field, you'll need to know a few things. Because fields are perceived by the server in an unpredictable sequence, you'll need a field's intended sequence on the form in order to present errors in the right order. You'll need to know what tests should be performed on the field. And, you'll need the field label that should appear in error messages.

One way to accomplish this is to create one hidden field for each real field to be checked. Give the hidden field the same name as the real field except for adding a _t to the hidden field's name. For the value of the hidden field, specify three things separated by tildes: a sequence value that will place the field in the proper sequence if alphabetically sorted, a list of names of tests to be applied to the field, and the field's label. The latter two pieces of information need to be available somewhere to support client-side checks anyway. The sequence value is the only additional piece of information needed to support server-side checks.

Sometimes custom checks are needed to handle unique situations on a specific form. To provide custom checks, the partial name of a page containing such checks should be known. Why a partial name? Because the same basic name can be used for a file that holds custom checks for the client side. For example, the client-side custom code might be in "Form42.js" and the server-side custom code might be in "Form42.cfm".

Notice that a form that provides test specifications in hidden fields and provides the partial name of a file containing custom code will readily support both client-side and server-side checks.

Sample Form and Destination

Put this sample form in MyForm2.cfm. Begin by stating "Customs": the partial filename of the page that contains custom code for this form. Client-side checks would be included next within script tags. Include server-side checks it the form has been submitted; they will stop processing if they see a problem. Provide a destination for the user to go to if the form is submitted successfully. Once the new destination has been included, stop processing this page.

<!--- Tell standard code the partial name
of the page that holds custom code --->
<cfset Customs="Form42">

<!---
Client-side checks would be included here
within script tags; formCheck, for example
--->

<!--- Act when the form is submitted --->
<cfparam name="form.Go" default="">
<cfif len(trim(form.Go))>
  <cfinclude template="ServCheck.cfm">
  <!--- which will stop on error, else... --->
  <cfinclude template="MyAct.cfm">
  <cfabort>
</cfif>

For this demonstration, the form is worded as an invitation to play a TV game show. Open the form. Copy the Customs variable to a hidden field to provide future flexibility. If this is not done and you choose later to have the form target be a page other than the form itself, the Customs variable needs to be received by the server-side checks in this manner. (If you'll ALWAYS submit your forms to themselves as shown in this example, you won't need to use this field. It's safest to code it now for the future.) Name the game.

<form name="MyForm" method="post">
<!---
this attribute would call client-side checks:
onSubmit="return formCheck(this)"
--->

<!--- Pass "Customs" to the server in case
the form doesn't submit to itself;
support server-side checks --->
<input type="hidden" name="Customs"
value=<cfoutput>"#Customs#"</cfoutput>>

<!--- Time to play... --->
Let's break a deal [grin]
<p>

Provide a select box that asks if the user wants to play. Note that the first option consists of instructions to the user, not a choice. Follow it with a corresponding hidden form that provides a sequence, a list of tests (required and custom) and a label to become part of associated error messages. Note that the entire message isn't needed here, just the label. Note that a single hidden field names as many tests as you want for the corresponding "real" field.

The sequence value deserves further discussion. Because the code to sort these test specifications will look at the entire specification and not just the sequence value at the beginning of it, an alphabetical sort must be performed. Therefore, the sequence value from the form should always be evenly padded to survive an alphabetical sort and to avoid having test names influence the order of short sequence values.

Do you want to play?<br>
<select name="Play">
<option value="" selected>Decide
<option value="Yes">Yes
<option value="No">No
</select>
<input type="hidden" name="Play_t"
value="01~required custom~Play decision">
<p>

Now add a text field with its hidden field and a set of radio buttons with its hidden field.

How much money will you risk?<br>
<input type="text" name="Money" value="">
<input type="hidden" name="Money_t" 
value="02~required number gezero~Money to risk">
<p>

Which do you want to open:<br>
<input type="radio" name="Door"
value="1"> door number 1,<br>
<input type="radio" name="Door"
value="2"> door number 2,<br>
<input type="radio" name="Door"
value="3"> door number 3,<br>
<input type="radio" name="Door"
value="stop"> or stop and keep the money?<br>
<input type="hidden" name="Door_t"
value="03~required~Door to open">
<p>

Toss in a textarea with its hidden field, a set of checkboxes with its hidden field, and a multiple select box with its hidden field. Be sure to use the word "multiple" in the select tag to let the user make multiple choices.

What do you think is behind it?<br>
<textarea name="Guess" rows="3" cols="45"></textarea>
<input type="hidden" name="Guess_t"
value="04~required custom~Your guess">
<p>

Pick ways that you'll show you're having fun:<br>
<input type="checkbox" name="Fun" value="sing">sing,<br>
<input type="checkbox" name="Fun" value="grin">grin<br>
<input type="hidden" name="Fun_t"
value="15~required~Ways to show you're having fun">
<p>

You'll need to change clothes during competition.
What colors will you wear?<br>
<select name="Colors" multiple>
<option value="" selected>Decide
<option value="Red">Red
<option value="White">White
<option value="Blue">Blue
</select>
<input type="hidden" name="Colors_t"
value="16~required~Clothing colors">
<p>

To provide an excuse to practice limiting the character set, ask for the user's last name. Provide a submit button and close the form.

What's your last name?
<input type="input" name="LastName">
<input type="hidden" name="LastName_t"
value="20~limited~Last name">

<input type="submit" name="Go" value="Respond">

</form>

Sample Action

Put this one line in MyAct.cfm.

SUCCESS!

Core Test

Put this code in ServCheck.cfm. You can use it for every form constructed in this manner.

Because of the way MyForm2.cfm was constructed, fields whose names end in _t contain test specifications (Specs) formatted as {sequence}~{tests}~{label} corresponding to a "real" field whose name can be determined by removing the _t from the name of the specification field (a hidden field). Therefore, you'll need code that puts them in a list (adding the name of the real field to each specification), puts the list in the specified sequence, applies the tests named by the specification, and displays errors after all fields have been checked.

Begin by initializing some variables: the specification list to be filled and sorted, the separator to be used between specifications, a value for a line separator to format the error message, and the separator (a tilde) that is used between elements of the specification.

Then, loop the keys (fieldnames) of the form structure. If the fieldname contains _t, add its contents to the specification list, define the real name of the field to be checked as the current fieldname with the _t removed, tack that on as well (separated by the element separator, and tack on the specification separator. Repeat for all fields.

<!--- Get a Specification List --->
<cfset SpecList="">
<cfset SpecSep="|">
<cfset LineSep="#chr(10)#">
<cfset ElementSep="~">
<cfloop list="#structKeyList(form)#" index="FieldName">
<cfif FieldName contains "_t">
  <cfset RealName=left(FieldName,len(FieldName)-2)>
  <cfset SpecList=SpecList&form[FieldName]&
  ElementSep&RealName&SpecSep>
</cfif>
</cfloop>

Because the sort will look at the entire specification, not just the sequence at the beginning of it, an alphabetical (text) sort must be performed. Make it ascending (asc). Indicate the character that separates the long specification list (SpecList) into the chunks to be sorted (SpecSep).

<!--- Alphabetically sort the list;
specifications must support an alphabetical sort --->
<cfset SpecList=listSort(SpecList,"text","asc","#SpecSep#")>

Loop through the sorted string of test specifications, performing appropriate tests for each real field. Begin by parsing the specification into its list of test names (Tests), its label (Label), and the real name (RealName) of the field to be checked. Then, use the real name to get the data value to be checked.

<!--- Check each field; build the error message --->
<cfset Msg="">
<cfloop list="#SpecList#" index="Spec" delimiters="#SpecSep#">
<!--- (Parse the Spec) --->
<cfset Tests=listGetAt(Spec,2,ElementSep)>
<cfset Label=listGetAt(Spec,3,ElementSep)>
<cfset RealName=listGetAt(Spec,4,ElementSep)>

<!--- Get the data value to be checked --->
<cfset DataValue="">
<cfif structKeyExists(form,RealName)>
  <cfset DataValue=structFind(form,RealName)>
</cfif>

A one-pass loop can make code easier to follow by letting you break to the end of a long sequence. Begin a one-pass loop that will span all tests. The existence of this loop lets you skip remaining tests at any point through the use of a break statement so you can proceed directly to the next specification.

After the loop begins, start testing. First, see if the field is empty. If so, and it's required - it's contained in the list of Tests for this field - complain by adding to a cumulative error message (Msg). Begin each complaint with a line separator (LineSep) so the error messages don't run together. Then for standard errors, provide the label followed by canned text for the error.

Include custom code if appropriate. The custom code will add error text to a string named TempMsg if it has any errors; so, append TempMsg to the cumulative error message.

Finally, because there is nothing else you can do with an empty input, break the loop to skip remaining tests in order to immediately work with the next field.

<!--- This one-pass loop lets you skip
to the next specification when desired --->
<cfloop from=1 to=1 index="Pass">

<!--- If empty, skip all tests but the "required"
and "custom" tests --->
<cfif not len(trim(DataValue))>
  <!--- Empty --->
  <cfif Tests contains "required">
    <cfif not len(trim(DataValue))>
      <cfset Msg=Msg&LineSep&Label&" must not be empty">
    </cfif>
  </cfif>
  <!--- Custom... --->
  <cfif Tests contains "custom">
    <cfinclude template="#Customs#.cfm">
    <cfset Msg=Msg&TempMsg>
  </cfif>
  <cfbreak>
</cfif>

If the field is not empty, then apply other standard tests named in the specification. The first of these tests should be applied automatically unless it is overridden: a check for bad characters. Regular expressions one would use in javascript to test a value can also be used with very little adjustment by ColdFusion's reFind tag. (Double pound signs. Double double quotes.) For this demo, reject angle brackets, percent signs (due to interference with database searches), pound signs, asterisks and tildes (due to interference with a standard business format known as X12 which may be used for the back end of an application), and double quotes (which some applications may not handle correctly).

<!--- If the "any" override is NOT present,
complain if input contains bad characters --->
<cfif Tests does not contain "any">
  <cfif reFind("[%##\>\*\""\<~]+",DataValue)>
    <cfset Msg=Msg&LineSep&Label&
    ' cannot contain %, ##, >, *, ", <, or ~'>
  </cfif>
</cfif>

If you've seen one test, you've seen them all; well, nearly all. Most tests can be implemented through the use of regular expressions with greater speed and brevity (if perhaps a loss of clarity for some audiences) when compared with traditional if-then logic. If the "number" test should be made, see if the field consists of an optional sign, zero or more digits, an optional decimal point, and zero or more digits following the decimal point. (You've already skipped this test if the field is empty; so, it won't be empty.)

<!--- Complain if it should be a number but isn't --->
<cfif Tests contains "number">
  <cfif not reFind("^[-+]?(([0-9]+\.?[0-9]*)|(\.[0-9]+))$",DataValue)>
    <cfset Msg=Msg&LineSep&Label&" must be a number">
  </cfif>
</cfif>
It would be nice to be sure a number is not negative. To see if the value is greater than or equal to zero (via a test named "gezero"), a simple less-than test will suffice.

<!--- Complain if it should be greater than or
equal to zero but isn't --->
<cfif Tests contains "gezero">
  <cfif val(DataValue) lt 0>
    <cfset Msg=Msg&LineSep&Label&
    " must be greater than or equal to zero">
  </cfif>
</cfif>

For this demonstration, someone's last name is unlikely to have characters other than letters and a hyphen. You can use reFindNoCase to avoid having to check letter case in the regular expression itself. If the field should use this limited character set, complain if it contains anything other than a letter a-z or a hyphen.

<!--- Complain if contents exceed limited character set --->
<cfif Tests contains "limited">
  <cfif reFindNoCase("[^a-z\-]",DataValue)>
    <cfset Msg=Msg&LineSep&Label&
    " must contain only letters and hyphens">
  </cfif>
</cfif>

You can add as many tests as you like in the same manner as the ones above. Finish by applying the custom code if the list of tests for the field calls for this to be done. The custom code will add to TempMsg if an error has to be displayed; add TempMsg to the cumulative error message string.

To work with the next field, close two loops: the one-pass loop created for convenience, and the loop that's actually reviewing each specification.

<!--- Custom... --->
<cfif Tests contains "custom">
  <cfinclude template="#Customs#.cfm">
  <cfset Msg=Msg&TempMsg>
</cfif>

<!--- End one-pass loop --->
</cfloop>

<!--- Test the next field --->
</cfloop>

Because these errors will be written directly to a page instead of winding up in an alert box, no mechanism is needed to be sure the box doesn't exceed the screen. Just spit out all the errors at once. One approach would be to simply print the entire line as a variable (since you've put line separators in it). The possibly redundant (but foolproof) approach taken here is to loop the error message (if it's not empty), split it on line separator, and insert an HTML break between lines.

The final message needs to tell the user to press the back button to correct and re-submit the page. This is the most straightforward approach. End by blocking further processing of the page.

<cfif len(Msg)>
  <cfloop list="#Msg#" index="Line" delimiters="#LineSep#">
  <cfoutput>#Line#</cfoutput><br>
  </cfloop>
  Press your back button, correct any problems, and re-submit.
  <cfabort>
</cfif>

Custom Tests

Put this code in Form42.cfm. The filename is based on the partial filename provided by "Customs" in the including page and will be different for each form you create that needs custom code. The custom code will add to TempMsg if an error has to be displayed. So, empty TempMsg.

If a field requires a custom test, this page gets included without specifying which custom test is needed. So, check Realname to determine if a given test should be applied to this field. This sample defines two tests: one that looks at multiple fields, and one that performs a non-standard check of a single field. One is invoked for the field named "Play"; the other is for the field named "Guess".

<!--- Apply appropriate custom tests; note errors --->
<cfset TempMsg="">
<cfif RealName is "Play">
  <cfif DataValue is "No">
    <cfif len(trim(form.Money))>
      <cfset TempMsg=TempMsg&LineSep&
      "Reconsider your decision not to play.  "&
      "Don't specify an amount to risk "&
      "if you won't play">
    </cfif>
  </cfif>
<cfelseif RealName is "Guess">
  <cfif DataValue contains "nothing">
    <cfset TempMsg=TempMsg&LineSep&
    "We wouldn't provide an empty showcase.  "&
    "Make a better guess.">
  </cfif>
</cfif>

Extensions

This is an easy way to add server-side validation to all forms you will write. Write and specify more tests as desired. Browse myform2.cfm.

If you're concerned that someone may rewrite your form, omitting or changing the type or content of its fields, here are some things to consider.

Finally, notice that you have to put the specifications somewhere. Programmers have traditionally instantiated them in newly written code for each field [groan]. The specifications would be more useful if stored in a database, however. The code in this tip that checks each field could have received its test specifications from a query as easily as from a form structure. Further, you could have used a query to define the form's field labels and attributes in the first place (and even the values of the hidden fields). [Hint, hint.] So, write these tests once and use them whenever they're needed. A good utility is a gift that keeps on giving. =Marty=