Chapter 15. Data Validation: Client, Server, or Both

In Chapter 14, you saw how to add Ajax to an XHTML form to asynchronously send user data between the client and the server. Somewhere in the application, that data should be checked—or validated—to determine whether it is the type of data that the program expected. This chapter will look at ways that validation can happen within an Ajax application, and where the validation should take place. Then we can see what benefits Ajax can bring to form validation to make your web application more robust.

Data Validation Is Important

Any developer who doubts the importance of data validation should think again. In fact, I would call such a developer crazy. The old paradigm “garbage in, garbage out” is extremely dangerous in any environment where the developer cannot control the users of an application. Crashes, hacks, and undesirable results can occur when the user is left to his own devices regarding the information he sends to the server or any other part of the client application. We’ll discuss several scenarios that demonstrate the danger of collecting data from a user without checking what that user entered before letting the program have at it.

First, imagine you have built a form that collects emergency contact information from a user and stores it in a database. In several places in this scenario, it would be important to have some validation around the form:

  • Is there a valid-looking phone number?

  • Was a name entered?

  • Was a relationship selected?

All of these would be important fields to validate; after all, a name is necessary, a phone number with at least the correct syntax is required, and it would be good to have the relationship of the contact.

Here’s a second scenario: imagine you built a form that allowed a user to log in to a site where security is a requirement. This is a potentially dangerous case where a malicious user could try to access a system she does not have a right to access. In this type of attack, called a SQL injection attack, the user attempts to fool the system by placing SQL code in the form field. Most likely, the JavaScript on the client side is just checking to make sure something was entered in the field and not that the field looks like a password.

The code on the server is responsible for filtering out bad data to prevent attacks of this nature. To give you a better idea of the scenario, consider the following code used to change a password:

SELECT
    id
FROM
    users
WHERE
    username = '$username' AND
    password = '$password';

Now pretend that the user enters the following password and the system has no safeguards against this sort of thing:

secret' OR 'x' = 'x

You can see how a clever individual could enter a SQL script such as this and gain access to things she should not. In this case, the SQL injection would allow the user to log in to the system without actually knowing the password. The SQL that would be passed to the server would look like this:

SELECT
    id
FROM
    users
WHERE
    username = 'Anthony' AND
    password = 'secret' OR 'x'='x';

To prevent this sort of scenario, many languages provide ways to strip out potential problem code before it becomes a problem. In PHP’s case, it provides the mysql_real_escape_string( ) function, which you can use like this:

<?php
/* Protect the query from a SQL Injection Attack */
$SQL = sprintf("SELECT id FROM users WHERE username='%s' AND password='%s'",
    mysql_real_escape_string($username),
    mysql_real_escape_string($password)
);
?>

Frameworks such as Zend provide a wrapper for this functionality; for Zend it is the following:

<?php
/* Protect the query from a SQL Injection Attack - the Zend way */
$sql = $db->quoteInto('SELECT id FROM users WHERE username = ?', $username);
$sql .= $db->quoteInto(' AND password = ?', $password);
?>

As you can see from the two example scenarios, validating the data passed from a form is important for both the client and the server. They do not necessarily check for the same things, but they both have their own duties. Table 15-1 summarizes these duties.

Table 15-1. Validation duties of the client and server

Duty

Client

Server

Check for null and blank values.

X

X

Check for syntax errors.

X

 

Check for type errors.

X

X

Check for potential hacks.

 

X

The contents of Table 15-1 basically say that anything that could harm the server if client validation were to fail in some way should be validated on the server, while the server should check for specialized attacks, and the client should check for reasons not to send the form to the server in the first place.

Validation with JavaScript

JavaScript’s main job in terms of its role in validation is to keep forms from being sent to the client when something is obviously wrong with them. Things are obviously wrong with a form when required fields have not been filled in, but there are other issues to check for as well. One is that the value in the field is an expected type—there should not be characters in a field where a number is expected, for example. Finally, there is the obvious consideration of whether the syntax of a given field is in a format that is expected—a phone number missing an area code, for instance.

The point of this kind of validation is to reduce the load on a server that has the potential for a lot of traffic, especially if the page in question is part of a web application that many people use. Whenever possible, checking should be done on the client, at the cost of the client CPU rather than the server CPU.

Value Checking

An easy form of client-side validation using JavaScript is to check fields for values. In these situations, you know what you are or are not looking for, and all you need to do is simply check the field. First, on any form field (especially those that the form requires), you need to make sure there is a value in the field. It does not do a whole lot of good to try to check for field types, syntaxes, and so on when there is nothing to check.

For example, I find out whether the field is null or blank in some way. Some methods for doing this include checking for null, seeing whether the field holds an empty string, and checking whether the field length is 0. Here is an example of this sort of function:

/**
 * This function, isNull, checks to see if the passed parameter /p_id/ has a
 * value that is null or not.  It also checks for empty strings, as form values
 * cannot really be null.
 *
 * @param {String} p_id The name of the input field to get the value from.
 * @return Returns a value indicating whether the passed input is a valid number
 *     or not.
 * @type Boolean
 */
function isNull(p_id) {
    try {
        p_id = $F(p_id);
        return (p_id == null || p_id == ''),
    } catch (ex) {
        return (true);
    }
}

Another easy test is to ensure that the value needed is being entered. This involves testing whether the values are equal, as in the following code:

/**
 * This function, testValue, checks to see if the passed /p_id/ has a value that
 * is equal to the passed /p_value/ in both value and type.
 *
 * @param {String} p_id The name of the input field to get the value from.
 * @param {Number | String | Boolean | Object | Array | null | etc.} p_value The
 *     value to test against.
 * @return Returns a value indicating whether the passed inputs are equal to one
 *     another.
 * @type Boolean
 */
function testValue(p_id, p_value) {
    try {
        /* Check both value and type */
        return($F(p_id) === p_value);
    } catch (ex) {
        return (false);
    }
}

You will notice in this example that I am using the inclusive === operator to test for equality. This means I want to make sure the two values are the same in type and value. If your needs differ and value is all you need to check for, change this to use the == operator instead.

The test I have not yet discussed is that of field type. You can use two different approaches in this case. The first is to use JavaScript’s built-in functions and operators. For example, you could use any of the following functions: parseInt( ), parseFloat( ), isFinite( ), or isNaN( ). For user-defined types, however, these functions do not do the trick and you need something else. This is where you can turn to regular expressions.

Using Regular Expressions

When you need to check for more complex data formats, syntax, or values, your best solution is to use regular expressions. Oftentimes, developers either forget all about regular expressions, or are afraid of them. I admit they are rather daunting until you become more familiar with them, but once you do, you will see how useful they can be. Just pretend they are not a part of the subject of theoretical computer science!

Regular expressions can parse a string much more efficiently and effectively than writing the code to do the parsing yourself. They work by comparing patterns with strings to find matches. For example, to match the strings “color” and “colour,” you can use the pattern colou?r. Example 15-1 gives some basic examples of some common regular expressions.

Example 15-1. Common regular expressions

/[A-Z][A-Z]/  // State abbreviation

/^(.|
){0,20}$/  // Limit the size of a string

/[1-9]d{4}-d{4}/  // US Zip code

/* IP4 address */
/(([01]?d?d|2[0-4]d|25[0-5]).){3}([01]?d?d|2[0-4]d|25[0-5])/

/* US dates */
/^[0,1]?d{1}/(([0-2]?d{1})|
    ([3][0,1]{1}))/(([1]{1}[9]{1}[9]{1}d{1})|([2-9]{1}d{3}))$/

Tip

Regular expressions are well outside the scope of this book, although they are a fascinating subject. For more information on them, check out Mastering Regular Expressions by Jeffrey E. F. Friedl (O’Reilly).

Specialized Data Checking

Taking what we now know about regular expressions, we can apply them to more specific type checks that give us much greater flexibility in what the client-side validation checks. Now, user-defined types such as phone number, email address, and credit card number can be checked (at least for syntax) before being passed along to the server.

Phone numbers

Phone numbers are fields found in many forms, and although we have the means to check whether any number was entered into the field, we are more limited in what else we can check. Sure, a developer could test the string length, and if it was within an accepted range of values, the field could pass a test. But what about testing to make sure that it is in a format that our backend system can handle before even giving the server the number to parse? Here is where using a regular expression can improve a phone number check, as the following example demonstrates:

/**
 * This function, isPhoneNumber, checks the syntax of the passed /p_id/ and
 * returns whether it is a valid US phone number in one of the following
 * formats:
 *     - (000) 000-0000
 *     - (000)000-0000
 *     - 000-000-0000
 *     - 000 000 0000
 *     - 0000000000
 *
 * @param {String} p_id The name of the input field to get the value from.
 * @return Returns a value indicating whether the passed input has a valid US
 *     phone number format.
 * @type Boolean
 */
function isPhoneNumber(p_id) {
    try {
        return (/^(?[2-9]d{2}[)-]?s?d{3}[s-]?d{4}$/.test($F(p_id)));
    } catch (ex) {
        return (false);
    }
}

Breaking down the regular expression a bit, the first part, (?[2-9]d{2}[)-]?, gives the option of an opening parenthesis, a three-digit area code that starts with a number between 2 and 9, and an optional closing parenthesis followed by an optional dash. Following this is the second part, s?d{3}[s-]?, which gives a possible space, and then a three-digit prefix followed by an optional space or dash. The last part, d{4}, checks for the four-digit suffix. It is not perfect by any means, but it is a lot better than the alternatives.

Email addresses

Checking for a valid email address would also result in pretty poor validation without regular expressions. Most developers need to do more than just check to see whether the email address contains an at character (@). The following is an example of a pretty robust email check:

/**
 * This function, isValidEmail, indicates whether the passed variable has a
 * valid email format.
 *
 * @param {String} p_id The name of the input field to get the value from.
 * @return Returns a value indicating whether the passed input has a valid
 *     email format.
 * @type Boolean
 */
function isValidEmail(p_id) {
    try {
        return (/^(([^<>( )[]\.,;:s@"]+(.[^<>( )[]\.,;:s@"]+)*)|
            (".+"))@(([(2([0-4]d|5[0-5])|1?d{1,2})(.(2([0-4]d|5[0-5])|
            1?d{1,2})){3} ])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/.test(
            $F(p_id)));
    } catch (ex) {
        return (false);
    }
}

This regular expression is very messy, I admit. In a nutshell, this expression checks the string for any invalid characters that would automatically invalidate it before going on to check the string. This string checks that the email address has an addressee, followed by the @ and then the domain. The domain can be an IP address or any domain name.

Warning

This regular expression checks the syntax of the domain for the email address, but it cannot check to see whether the domain is actually valid. The server is the proper place to make this check, provided that it is fast enough to do so.

Social Security numbers

Social Security numbers follow the format XXX-XX-XXXX. The first group of numbers is assigned by state, territory, and so on and is any series from 001-772 (as of this writing). The second group of numbers is assigned based on a formula (I do not know what it is), and the final group of numbers is sequential from 0001-9999. The following code demonstrates this:

/**
 * This function, isSSN, checks the passed /p_id/ to see whether it is a valid
 * Social Security number in one of the following formats:
 *     - 000-00-0000
 *     - 000 00 0000
 *     - 000000000
 *
 * @param {String} p_id The name of the input field to get the value from.
 * @return Returns a value indicating whether the passed input has a valid
 *     SSN format.
 * @type Boolean
 */
function isSSN(p_id) {
    try {
        if (!(/^d{3}(-|s?)d{2}1d{4}$/.test($F(p_id))))
            return (false);
        var temp = $F(p_id)

        /* Strip valid characters from number */
        if (temp.indexOf('-') != -1)
            temp = (temp.split('-')).join(''),
        if (temp.indexOf(' ') != -1)
            temp = (temp.split(' ')).join(''),
        return ((temp.substring(0, 3) != '000') && (temp.substring (3, 5)
!= '00') && (temp.substring(5, 9) != '0000'));
    } catch (ex) {
        return (false);
    }
}

Tip

A Social Security number cannot comprise all zeros, as in 000-00-0000. A separate check is used to test for these occurrences.

Credit cards

An accredited company must properly validate a credit card before an online store will accept the card number, and this validation should be done on the server side of things. The client can still make sure the card number has the correct number of digits based on its type, and whether the digits make sense. It does this using the Luhn Formula, which tests digits by using a modulus 10 checksum as the last digit in the number. Table 15-2 shows some basic information available on credit cards that use this method to issue card numbers.

Table 15-2. Acceptable values for certain credit cards

Card type

Valid prefixes

Valid length

American Express

34 or 37

15

Diners Club

30, 36, or 38

14

Discover

6011

16

MasterCard

51–55

16

Visa

4

16

The following code example uses a variation to the Luhn Formula to acquire a checksum, to account for cards with even and odd digits:

/**
 * This function, isValidCreditCard, checks to see if the passed
 * /p_cardNumberId/ is a valid credit card number based on the passed
 * /p_cardTypeId/ and the Luhn Formula.  The following credit cards may be
 * tested with this method:
 *     - Visa has a length of 16 numbers and starts with 4 (dashes are optional)
 *     - MasterCard has a length of 16 numbers and starts with 51 through 55
 *       (dashes are optional)
 *     - Discover has a length of 16 numbers and starts with 6011 (dashes are
 *       optional)
 *     - American Express has a length of 15 numbers and starts with 34 or 37
 *     - Diners Club has a length of 14 numbers and starts with 30, 36, or 38
 *
 * @param {String} p_cardTypeId The name of the input field to get the card
 *     type from.
 * @param {String} p_cardNumberId The name of the input field to get the card
 *     number from.
 * @return Returns whether the card number is in the correct syntax and has a
 *     valid checksum.
 * @type Boolean
 */
function isValidCreditCard(p_cardTypeId, p_cardNumberId) {
    var regExp = '';
    var type = $F(p_cardTypeId);
    var number = $F(p_cardNumberId);

    /* Is the card type Visa? [length 16; prefix 4] */
    if (type == "VISA")
        regExp = /^4d{3}-?d{4}-?d{4}-?d{4}$/;
    /* Is the card type MasterCard? [length 16; prefix 51 - 55] */
    else if (type == "MasterCard")
        regExp = /^5[1-5]d{2}-?d{4}-?d{4}-?d{4}$/;
    /* Is the card type Discover? [length 16; prefix 6011] */
    else if (type == "Discover")
        regExp = /^6011-?d{4}-?d{4}-?d{4}$/;
    /* Is the card type American Express? [length 15; prefix 34 or 37] */
    else if (type == "AmericanExpress")
        regExp = /^3[4,7]d{13}$/;
    /* Is the card type Diners Club? [length 14; prefix 30, 36, or 38] */
    else if (type == "Diners")
        regExp = /^3[0,6,8]d{12}$/;
    /* Does the card number have a valid syntax? */
    if (!regExp.test(number))
        return (false);
    /* Strip valid characters from number */
    number = (number.split('-')).join(''),
    number = (number.split(' ')).join(''),

    var checksum = 0;
    /* Luhn Formula */
    for (var i = (2 - (number.length % 2)), il = number.length; i <= il; i += 2)
        checksum += parseInt(number.charAt(i - 1));
    for (var i = (number.length % 2) + 1, il = number.length; i < il; i += 2) {
        var digit = parseInt(number.charAt(i - 1)) * 2;

        checksum += ((digit < 10) ? digit : (digit - 9));
    }
    return (!(checksum % 10) && checksum)
}

A Validation Object

To sum up all of the tests I have shown so far, it makes sense to create an object based on Prototype’s Form object that can handle all of our validation needs. Example 15-2 shows what this object looks like.

Example 15-2. validation.js: The Form.Validation object

/**
 * @fileoverview, This file, validation.js, encapsulates some of the basic methods
 * that can be used to test values, types, and syntax from within JavaScript before
 * the form is sent to the server for processing.
 *
 * This code requires the Prototype library.
 */

/**
 * This object, Form.Validation, is an extension of the Prototype /Form/ object and
 * handles all of the methods needed for validation on the client side.  It consists
 * of the following methods:
 *     - isNull(p_id)
 *     - isNumber(p_id)
 *     - isMoney(p_id)
 *     - testValue(p_id, p_value)
 *     - isValidDate(p_id)
 *     - isPhoneNumber
 *     - isValidEmail
 *     - isSSN
 *     - isValidCreditCard
 */
Form.Validation = {
    /**
     * This method, isNull, checks to see if the passed parameter /p_id/ has a
     * value that is null or not.  It also checks for empty strings, as form values
     * cannot really be null.
     *
     * @member Form.Validation
     * @param {String} p_id The name of the input field to get the value from.
     * @return Returns a value indicating whether the passed input is a valid
     *     number or not.
     * @type Boolean
     */
    isNull: function(p_id) {
        try {
            p_id = $F(p_id);
            return (p_id == null || p_id == ''),
        } catch (ex) {
            return (true);
        }
    },
    /**
     * This member, isNumber, checks to see if the passed /p_id/ has a value that
     * is a valid number. The method can check for the following types of number:
     *     - 5
     *     - -5
     *     - 5.235
     *     - 5.904E-03
     *     - etc., etc., etc....(you get the idea, right?)
     *
     * @member Form.Validation
     * @param {String} p_id The name of the input field to get the value from.
     * @return Returns a value indicating whether the passed input is a valid
     *     number or not.
     * @type Boolean
     */
    isNumber: function(p_id) {
        try {
            return (/^[-+]?d*.?d+(?:[eE][-+]?d+)?$/.test($F(p_id)));
        } catch (ex) {
            return (false);
        }
    },
    /**
     * This member, isMoney, checks to see if the passed /p_id/ has a value that
     * is a valid monetary value.  The method can check for the following types of
     * number:
     *     - 250
     *     - -250
     *     - 250.00
     *     - $250
     *     - $250.00
     *
     * @member Form.Validation
     * @param {String} p_id The name of the input field to get the value from.
     * @return Returns a value indicating whether the passed input is a valid
     *     monetary value or not.
     * @type Boolean
     */
    isMoney: function(p_id) {
        try {
            return (/^[-+]?$?d*.?d{2}?$/.test($F(p_id)));
        } catch (ex) {
            return (false);
        }
    },
    /**
     * This method, testValue, checks to see if the passed /p_id/ has a value that
     * is equal to the passed /p_value/ in both value and type.
     *
     * @member Form.Validation
     * @param {String} p_id The name of the input field to get the value from.
     * @param {Number | String | Boolean | Object | Array | null | etc.} p_value
     *     The value to test against.
     * @return Returns a value indicating whether the passed inputs are equal to
     *     one another.
     * @type Boolean
     */
    testValue: function(p_id, p_value) {
        try {
            return($F(p_id) === p_value);
        } catch (ex) {
            return (false);
        }
    },
    /**
     * This method, isValidDate, checks to see if the passed /p_id/ has a value
     * that is a valid /Date/.  The method can check for the following date
     * formats:
     *     - mm/dd/yyyy
     *     - mm-dd-yyyy
     *     - mm.dd.yyyy
     * where /mm/ is a one- or two-digit month, /dd/ is a one- or two-digit day and
     * /yyyy/ is a four-digit year.
     *
     * After the format is validated, this method checks to make sure the value is
     * a valid date (i.e., it did or will exist.)
     *
     * @member Form.Validation
     * @param {String} p_id The name of the input field to get the value from.
     * @return Returns a value indicating whether the passed input is a valid date
     *     or not.
     * @type Boolean
     */
    isDate: function(p_id) {
        try {
            date = $F(p_id);
            /* Is the value in the correct format? */
            if (!/^d{1,2}(/|-|.)d{1,2}1d{4}$/.test(date))
                return (false);
            else {
                /*
                 * Find the separator for the different date parts, then split
                 * it up.
                 */
                var ds = /^/|-|.$/;

                ds = date.split(ds.exec(/^/|-|.$/), 3);
                /* Was there something to split? */
                if (ds != null) {
                    /* Check if this date should exist */
                    var m = ds[0], d = ds[1], y = ds[2];
                    var td = new Date(y, --m, d);

                    return (((td.getFullYear( ) == y) && (td.getMonth( ) == m) &&
                        (td.getDate( ) == d)));
                } else
                    return (false);
            }
        } catch (ex) {
            return (false);
        }
    },
    /**
     * This method, isPhoneNumber, checks the syntax of the passed /p_id/ and
     * returns whether it is a valid US phone number in one of the following
     * formats:
     *     - (000) 000-0000
     *     - (000)000-0000
     *     - 000-000-0000
     *     - 000 000 0000
     *     - 0000000000
     *
     * @member Form.Validation
     * @param {String} p_id The name of the input field to get the value from.
     * @return Returns a value indicating whether the passed input has a valid US
     *     phone number format.
     * @type Boolean
     */
    isPhoneNumber: function(p_id) {
        try {
            return (/^(?[2-9]d{2}[)-]?s?d{3}[s-]?d{4}$/.test($F(p_id)));
        } catch (ex) {
            return (false);
        }
    },
    /**
     * This method, isValidEmail, indicates whether the passed /p_id/ has a valid
     * email format.
     *
     * @member Form.Validation
     * @param {String} p_id The name of the input field to get the value from.
     * @return Returns a value indicating whether the passed input has a valid
     *     email format.
     * @type Boolean
     */
    isValidEmail: function(p_id) {
        try {
            return (/^(([^<>( )[]\.,;:s@"]+(.[^<>( )[]\.,;:s@"]+)*)|
            (".+"))@(([(2([0-4]d|5[0-5])|1?d{1,2})(.(2([0-4]d|5[0-5])|
            1?d{1,2})){3} ])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/.test(
            $F(p_id)));
        } catch (ex) {
            return (false);
        }
    },
    /**
     * This method, isSSN, checks to see if the passed /p_id/ is in a valid format
     * and returns whether it is a valid Social Security number in one of the 
     * following formats:
     *     - 000-00-0000
     *     - 000 00 0000
     *     - 000000000
     *
     * @member Form.Validation
     * @param {String} p_id The name of the input field to get the value from.
     * @return Returns a value indicating whether the passed input has a valid
     *     SSN format.
     * @type Boolean
     */
    isSSN: function(p_id) {
        try {
            if (!(/^d{3}(-|s?)d{2}1d{4}$/.test($F(p_id))))
                return (false);
            var temp = $F(p_id)

            /* Strip valid characters from number */
            if (temp.indexOf('-') != -1)
                temp = (temp.split('-')).join(''),
            if (temp.indexOf(' ') != -1)
                temp = (temp.split(' ')).join(''),
            return ((temp.substring(0, 3) != '000') &&
                (temp.substring(3, 5) != '00') &&
                (temp.substring(5, 9) != '0000'));
        } catch (ex) {
            return (false);
        }
    },
    /**
     * This method, isValidCreditCard, checks to see if the passed
     * /p_cardNumberId/ is a valid credit card number based on the passed
     * /p_cardTypeId/ and the Luhn Formula.  The following credit cards may be
     * tested with this method:
     *     - Visa has a length of 16 numbers and starts with 4 (dashes are
     *       optional)
     *     - MasterCard has a length of 16 numbers and starts with 51 through 55
     *       (dashes are optional)
     *     - Discover has a length of 16 numbers and starts with 6011 (dashes are
     *       optional)
     *     - American Express has a length of 15 numbers and starts with 34 or 37
     *     - Diners Club has a length of 14 numbers and starts with 30, 36, or 38
     *
     * @member Form.Validation
     * @param {String} p_cardTypeId The name of the input field to get the card
     *     type from.
     * @param {String} p_cardNumberId The name of the input field to get the card
     *     number from.
     * @return Returns whether the card number is in the correct syntax and has a
     *     valid checksum.
     * @type Boolean
     */
    isValidCreditCard: function(p_cardTypeId, p_cardNumberId) {
        var regExp = '';
        var type = $F(p_cardTypeId);
        var number = $F(p_cardNumberId);

        /* Is the card type Visa? [length 16; prefix 4] */
        if (type == "Visa")
            regExp = /^4d{3}-?d{4}-?d{4}-?d{4}$/;
        /* Is the card type MasterCard? [length 16; prefix 51 - 55] */
        else if (type == "MasterCard")
            regExp = /^5[1-5]d{2}-?d{4}-?d{4}-?d{4}$/;
        /* Is the card type Discover? [length 16; prefix 6011] */
        else if (type == "Discover")
            regExp = /^6011-?d{4}-?d{4}-?d{4}$/;
        /* Is the card type American Express? [length 15; prefix 34 or 37] */
        else if (type == "AmericanExpress")
            regExp = /^3[4,7]d{13}$/;
        /* Is the card type Diners Club? [length 14; prefix 30, 36, or 38] */
        else if (type == "Diners")
            regExp = /^3[0,6,8]d{12}$/;
        /* Does the card number have a valid syntax? */
        if (!regExp.test(number))
            return (false);
        /* Strip valid characters from number */
        number = (number.split('-')).join(''),
        number = (number.split(' ')).join(''),

        var checksum = 0;

        /* Luhn Formula */
        for (var i = (2 - (number.length % 2)), il = number.length; i <= il; i += 2)
            checksum += parseInt(number.charAt(i − 1));
        for (var i = (number.length % 2) + 1, il = number.length; i < il; i += 2) {
            var digit = parseInt(number.charAt(i - 1)) * 2;

            checksum += ((digit < 10) ? digit : (digit - 9));
        }
        return (!(checksum % 10) && checksum)
    }
};

Using Libraries to Validate

None of the JavaScript libraries, toolkits, or frameworks has done much in the way of validation—with the exception of the Dojo Toolkit. Dojo does have some validation capabilities regarding form elements, making it easier to check forms before sending them to the server. Dojo has two objects to handle validation: dojo.validate and dojo.validate.us. The dojo.validate.us object allows for U.S.-specific validation. Table 15-3 and Table 15-4 list the methods available for validation.

Table 15-3. Methods in the dojo.validate object

Method

Description

evaluateConstraint(profile, constraint, fieldName, elem)

This method checks constraints that are passed as array arguments, returning true or false.

getEmailAddressList(value, flags)

This method checks that the value passed in contains a list of email addresses using the optional flags, returning an array with the email addresses or an empty array if the value did not validate or was empty.

is12HourTime(value)

This method checks that the passed value is a valid time in a 12-hour format, returning true or false.

is24HourTime(value)

This method checks that the passed value is a valid time in a 24-hour format, returning true or false.

isCurrency(value, flags)

This method checks that the passed value denotes a monetary value using the optional flags, returning true or false.

isEmailAddress(value, flags)

This method checks that the passed value could be a valid email address using the optional flags, returning true or false.

isEmailAddressList(value, flags)

This method checks that the passed value could be a valid email address list using the optional flags, returning true or false.

isGermanCurrency(value)

This method checks that the passed value is a valid representation of German currency (euro), returning true or false.

isInRange(value, flags)

This method checks that the passed value denotes an integer, real number, or monetary value between a min and max found in the flags, returning true or false.

isInteger(value, flags)

This method checks that the passed value is in an integer format using the optional flags, returning true or false.

isIpAddress(value, flags)

This method checks that the passed value is in a valid IPv4 or IPv6 format using the optional flags, returning true or false.

isJapaneseCurrency(value)

This method checks that the passed value is a valid representation of Japanese currency, returning true or false.

isNumberFormat(value, flags)

This method checks that the passed value is any valid number-based format using the optional flags, returning true or false.

isRealNumber(value, flags)

This method checks that the passed value is in a valid real number format using the optional flags, returning true or false.

isText(value, flags)

This method checks that the passed value is a valid string containing no whitespace characters using the optional flags, returning true or false.

isUrl(value, flags)

This method checks that the passed value could be a valid URL using the optional flags, returning true or false.

isValidCreditCard(value, ccType)

This method checks that the passed value could be a valid credit card using the passed ccType, returning true or false.

isValidCreditCardNumber(value, ccType)

This method checks that the passed value could be a valid credit card number using the passed ccType, returning true or false.

isValidCvv(value, ccType)

This method checks that the passed value could be a valid credit card security number using the passed ccType, returning true or false.

isValidDate(dateValue, format)

This method checks that the passed dateValue could be a valid date using the passed format, returning true or false.

isValidLuhn(value)

This method checks that the passed value validates against the Luhn Formula to verify its integrity, returning true or false.

isValidTime(value, flags)

This method checks that the passed value could be a valid time using the passed flags, returning true or false.

Table 15-4. Methods in the dojo.validate.us object

Method

Description

isCurrency(value, flags)

This method checks that the passed value is a valid representation of U.S. currency using the optional flags, returning true or false.

isPhoneNumber(value)

This method checks that the passed value could be a valid 10-digit U.S. phone number in a number of formats, returning true or false.

isSocialSecurityNumber(value)

This method checks that the passed value could be a valid U.S. Social Security number, returning true or false.

isState(value, flags)

This method checks that the passed value is a valid two-character U.S. state using the optional flags, returning true or false.

isZipCode(value)

This method checks that the passed value could be a valid U.S. zip code, returning true or false.

The dojo.validate and dojo.validate.us objects rely on Dojo’s regular expression objects for all of the underlying checking. For example, the isRealNumber( ) method looks like this:

dojo.validate.isRealNumber = function(value, flags) {
    var re = new RegExp('^' + dojo.regexp.realNumber(flags) + '$'),
    return re.test(value);
}

Using the validation objects is straightforward. Example 15-3 will give you an idea how to use these objects within your code.

Example 15-3. Dojo validation in action

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    <head>
        <title>Example 15-3. Dojo validation in action.</title>
        <script type="text/javascript" src="dojo.js"> </script>
        <script type="text/javascript">
            //<![CDATA[
            dojo.require('dojo.widget.validate'),
            //]]>
        </script>
    </head>
    <body>
        <div>
            <form id="myDojoForm" name="myDojoForm" method="post"
                    action="dojoForm.php">
                <label for="myCurrency">Enter amount: <input type="text"
                        id="myCurrency" name="myCurrency"
                        value="" class="regular"
                        dojoType="CurrencyTextBox"
                        trim="true"
                        required="true"
                        cents="true"
                        invalidMessage="Invalid amount entered.  Include dollar
                            sign, commas, and cents." />
                </label>
                <input type="submit" value="Submit Amount" />
            </form>
        </div>
    </body>
</html>

CSS Notification of Errors

Most of the time when a validation problem has occurred somewhere on the form, a developer issues an alert telling the user there was a problem. Some developers go further and note in the alert where the problem is and then focus on that field after the user closes the alert. The problem is that sometimes it is still difficult to see where the problem is. Users need visual cues to quickly locate problems with the form so that they can be corrected. This is especially true when the form is long and there is a good chance that it will scroll on the page.

This is a good place for CSS rules to aid in visually telling the user where the problem is. You can also use CSS rules to indicate to the user where required fields are, and whether the form has everything it needs to be submitted.

CSS Error Rules

The rules that you can use to indicate form errors should be fairly simple and are meant only as an easy way for you to indicate problem fields. One good way to do this is to change the background color of an <input> element when it is of type text or password. This is also a good indicator for <textarea> elements. Consider the following:

input.text, textarea {
    background-color: #fff;
    border: 1px inset #999;
    color: #000;
    font: .8em Arial;
    margin: 1px 2px 3px;
    padding: 1px 3px;
}

input.error, textarea.error {
    background-color: #900;
    color: #fff;
}

All you need to do is to have a default setting for the fields and then a setting for when there is a problem. In the preceding code sample, the default values for the <input> element are set first, and then below them are the rules for the error indicator.

This is all well and good, but how do we alert the user to a problem when the <input> type is a radio button or checkbox? After all, as I indicated in Chapter 14, the developer has little to no control over the standard radio button or checkbox <input> elements.

An easy solution to this problem is to surround all radio buttons and checkbox choices with a <fieldset> element. Then you can set the error indicator to this element instead of attempting to manipulate the radio button or checkbox directly. The following shows a sample of the <fieldset> wrapper:

            <fieldset>
                <legend>Choice 1:</legend>
                <input id="choice1A" type="radio" class="otherInput"
                    name="choice1" value="A" />
                <label for="choice1A">A</label>
                <input id="choice1B" type="radio" class="otherInput"
                    name="choice1" value="B" />
                <label for="choice1B">B</label>
                <input id="choice1C" type="radio" class="otherInput"
                    name="choice1" value="C" />
                <label for="choice1C">C</label>
                <input id="choice1D" type="radio" class="otherInput"
                    name="choice1" value="D" />
                <label for="choice1D">D</label>
            </fieldset>
            <fieldset>
                <legend>Choice 2:</legend>
                <input id="choice2A" type="radio" class="otherInput"
                    name="choice2" value="A" />
                <label for="choice2A">A</label>
                <input id="choice2B" type="radio" class="otherInput"
                    name="choice2" value="B" />
                <label for="choice2B">B</label>
                <input id="choice2C" type="radio" class="otherInput"
                    name="choice2" value="C" />
                <label for="choice2C">C</label>
                <input id="choice2D" type="radio" class="otherInput"
                    name="choice2" value="D" />
                <label for="choice2D">D</label>
            </fieldset>

Figure 15-1 shows how our error rules look in the browser. The following is the CSS for the radio buttons and checkboxes:

fieldset {
    background-color: #fff;
    border: 2px outset #999;
    color: #000;
}

fieldset.error {
    background-color: #900;
    color: #fff;
}

fieldset.error legend {
    background-color: transparent;
    color: #000;
}
Example of error rules in the browser

Figure 15-1. Example of error rules in the browser

JavaScript Rule Switching

Once you have set up the rules to handle error indicators for the client, you need a mechanism to switch to CSS rules when applicable. This is simple—all you need to do is toggle the error rule for the field. Example 15-4 shows the function you can use to handle this.

Example 15-4. A simple example of CSS rule toggling

/*
 * Example 15-4.  A simple example of CSS rule toggling.
 */

/**
 * This function, toggleRule, toggles the passed /p_ruleName/ for the passed
 * /p_elementId/ in the page's form using Prototype's /Element.xxxClassName/
 * methods.
 *
 * @param {String} p_elementId The id of the element to toggle the rule for.
 * @param {String} p_ruleName The name of the CSS class that contains the rule to
 *     be toggled.
 * @return Returns whether the function was a success or not.
 * @type Boolean
 * @see Element#hasClassName
 * @see Element#removeClassName
 * @see Element#addClassName
 */
function toggleRule(p_elementId, p_ruleName) {
    try {
        if ($(p_elementId).hasClassName(p_ruleName))
            $(p_elementId).removeClassName(p_ruleName);
        else
            $(p_elementId).addClassName(p_ruleName);
        return (true);
    } catch (ex) {
        return (false);
    }
}

Validation on the Server

In terms of validation, the server script’s primary job (regardless of the language involved) is to protect the application from storing or parsing anything that could be harmful to it. It must check that it got the data it was expecting to get, because a form with only part of the necessary data is not very useful. The server script must protect itself from SQL injections and other attacks by hackers, as well as make sure that the correct values are being stored. Finally, the server script is responsible for informing the client of any problems it may have had in executing its functionality.

Did We Get What We Expected?

The first thing the server needs to check is whether it even received the data it was expecting. If the server script is expecting six parameters of data and gets only five, it might not be able to perform the operations it is meant to perform. For PHP, the easiest way to check on parameters is to test the $_REQUEST variable for the given parameter using the isset( ) or empty( ) language construct. The following code shows how to test for variables passed from the server in PHP:

<?php
/* Are the variables set that need to be? */
if (isset($_REQUEST['data1']) && isset($_REQUEST['data2']) &&
        isset($_REQUEST['data3'])) {
    // Do something here

    /* Do we have this variable? */
    if (isset($_REQUEST['data4'])) {
        // Do something else here
    } else {
        // We can live without data4
    }
}
?>

Warning

isset( ) returns whether the variable is set, whereas empty( ) checks whether the variable is empty. There is a difference between the two, as demonstrated here:

<?php
$data = 0;
empty($data); // TRUE
isset($data); // TRUE
$data = NULL;
empty($data); // TRUE
isset($data); // FALSE
?>

A value of 0 passed from the client would be considered empty, even though it is set. Be careful what you test with.

In terms of securing the server side from malicious data being sent from unknown sources, the $_REQUEST variable is not the best way to get data from the client. Instead, you should use the $_GET and $_POST variables depending on what the server script is actually expecting. This way, if the server is expecting a variable through a form POST, an attacker sending the same variable through a GET would not be able to find a hole. This is an easy way to protect yourself from attackers.

Protecting the Database

The server must protect itself from damage because it receives all the data requests without truly knowing where the data came from. It is easy to fake the client response from Telnet, a server-side script, or another web site. Because of this, the server must assume that it cannot trust any data coming from any client without first cleansing it of any potential bad characters.

We talked about the SQL injection attack earlier in the chapter, and we discussed how PHP can protect itself when you’re using MySQL with the mysql_real_escape_string( ) function. This is not the only use for this function. It also will encode characters that MySQL may have a conflict with when executing a statement.

Other languages may not have a function readily available to use against these issues. When this is the case, it is up to the developer to write the code to escape all potentially dangerous characters to the database so that nothing unexpected will happen when a statement is executed on the SQL server.

Value Checking on the Server

Besides protecting against database attacks, the server must also check the actual data coming from a client. Multiple layers of checking provide better security, and although the client will check the values with regular expressions and other means, you never know where the data is coming from. The server should never assume that the data came from the client, and should do its own value validation even if it was already done on the client.

The server should check the lengths of all values as well as their types, and apply regular expression validation against them to ensure that they are in the proper formats. The server has other responsibilities as well. Here is where it ensures that a Social Security number or credit card number actually exists, using services that provide this capability.

Only after the value has been checked by whatever means the server deems necessary should any data from the client be sent to a database or parsed by a server-side script. This will minimize any potential damage done to the server or the application and make the application on the whole more stable and secure.

Returning Problems

Whenever an application requires user input as part of its functionality, problems can occur with this data. In such situations, the server must alert the client of the problem so that it can be dealt with and communicated back to the user. What is actually returned need not be complicated as long as the client understands what it is getting. It can be as simple as returning 0 or false, and it can be the client’s responsibility to do more with the error once it is received. I showed you the code required for returning errors in this way already. Nothing more than this should be required of the server.

Ajax Client/Server Validation

Ajax provides the ability to check a user’s inputs in a more real-time manner. This can take some of the burden off the client, as it would no longer need to check every field value at once on a form submission. Instead, it checks fields as the user enters them, and it has the potential of speeding up the submission process, especially when the forms are longer. This capability will also lead to another feature that we will discuss in Chapter 16: search hiding and suggestions. First things first, though.

On-the-Fly Checking

Checking form fields on the fly is something that Windows applications can do, but it was not plausible on the Web until the advent of Ajax. Here’s how it works. Once the focus of a field blurs, a function is called, or a method in an object, that makes an asynchronous call to the server, thereby allowing the user to continue to work in the client while the validation takes place. The easiest way to do this is with the onblur( ) event on the <input> element, like this:

<input type="text" id="myElement" name="myElement" value="" onblur=return
Form.Validation.ajaxCheck(this, 'phone')," />

The simplest way to add this is to create some additional functionality in the Form.Validation object from Example 15-2. Our new method must validate on the client that it is not empty when the element blurs, and unless you wish to develop an extremely complicated method of parsing, it should also be sent the type of validation needed so that the Form.Validation knows what to use before sending it off to the server. Example 15-5 shows the new function.

Example 15-5. Added functionality for Ajax in the Form.Validation object

/*
 * Example 15-5. Added functionality for Ajax in the Form.Validation object.
 */

/**
 * This method, ajaxCheck, provides on-the-fly validation on the client and
 * server by using Ajax to allow the server to validate the passed /p_element/'s
 * value based on the passed /p_validationType/ and optional /p_options/
 * parameters.
 *
 * @member Form.Validation
 * @param {Object} p_element The element to validate.
 * @param {String} p_validationType The type of validation the client and
 *     server should provide.
 * @param {string} p_options Optional string to provide when validating credit
 *     cards to provide the card type.
 * @return Returns true so that the blur will happen on the passed element as
 *     it should.
 * @type Boolean
 * @see #isNumber
 * @see #isMoney
 * @see #isDate
 * @see #isPhoneNumber
 * @see #isEmail
 * @see #isSSN
 * @see #isValidCreditCard
 * @see #reportError
 * @see #reportSuccess
 * @see Ajax#Request
 */
ajaxCheck: function(p_element, p_validationType, p_options) {
    var validated = false;

    /* Is the validation type for validating a number? */
    if (p_validationType == 'number')
        validated = this.isNumber(p_element);
    /* Is the validation type for validating a monetary value? */
    else if (p_validationType == 'money')
        validated = this.isMoney(p_element);
    /* Is the validation type for validating a date? */
    else if (p_validationType == 'date')
        validated = this.isDate(p_element);
    /* Is the validation type for validating a phone number? */
    else if (p_validationType == 'phone')
        validation = this.isPhoneNumber(p_element);
    /* Is the validation type for validating an email address? */
    else if (p_validationType == 'isValidEmail')
        validation = this.isEmail(p_element);
    /* Is the validation type for validating a Social Security number? */
    else if (p_validationType == 'ssn')
        validation = this.isSSN(p_element);
    /* Is the validation type for validating a credit card? */
    else if (p_validationType == 'cc')
        validation = this.isValidCreditCard(p_options, p_element);
    /* Did client-side validation succeed? */
    if (validation) {
        new Ajax.Request('ajaxCheck.php', {
            method: 'get',
            parameters: {
                value: $F(p_element),
                type: p_validationType,
                options: p_options },
            onSuccess: function(xhrResponse) {
                /* Did the value not validate on the server? */
                if (xhrResponse.responseText == '0')
                    this.reportError(p_element.id, p_validationType);
                else
                    this.reportSuccess(p_element.id);
            }
        });
    } else
        this.reportError(p_element.id, p_validationType);
    return (true);
},
/**
 * This method, reportError, creates an element to put next to the form
 * field that did not validate correctly with a supplied message alerting the
 * user that there is a problem.
 *
 * @member Form.Validation
 * @param {String} p_element The element id to place the new element next to.
 * @param {String} p_validationType The type of validation the client and
 *     server provided.
 * @see #ajaxCheck
 * @see Element#addClassName
 */
reportError: function(p_id, p_validationType) {
    var message = '';

    /* Is the validation type for validating a number? */
    if (p_validationType == 'number')
        message = 'This field expects a number.  Example: 31';
    /* Is the validation type for validating a monetary value? */
    else if (p_validationType == 'money')
        message = 'This field expects a monetary value. Example: $31.00';
    /* Is the validation type for validating a date? */
    else if (p_validationType == 'date')
        message = 'This field expects a date. Example: 01/01/2007';
    /* Is the validation type for validating a phone number? */
    else if (p_validationType == 'phone')
        message = 'This field expects a phone number. Example (800) 555-5555';
    /* Is the validation type for validating an email address? */
    else if (p_validationType == 'isValidEmail')
        message = 'This field expects a valid email account. Example: ' +
            '[email protected]';
    /* Is the validation type for validating a Social Security number? */
    else if (p_validationType == 'ssn')
        message = 'This field expects a valid Social Security number. Example: ' +
            '234-56-7890';
    /* Is the validation type for validating a credit card? */
    else if (p_validationType == 'cc')
        message = 'This field expects a valid credit card number. Example: ' +
            '4123 4567 8901 2349';
    /* There was an unknown validation type */
    else
        message = 'The input in this field is invalid.';

    var span = document.createElement('span'),

    span.appendChild(document.createTextNode(message));
    span.id = 'span' + p_id;
    Element.addClassName(span, 'validationError'),
    $(p_id).parentNode.appendChild(span);
},
/**
 * This method, reportSuccess, checks to see if the passed /p_id/ has a
 * sibling element.  If it does and that element is a <span> element with a class
 * name of /validationError/ then remove it.
 *
 * @param {String} p_element The element id to check for another element next to it.
 * @see #ajaxCheck
 * @see Element#hasClassName
 */
reportSuccess: function(p_id) {
    var elem = $(p_id);

    /* Does the element have another element next to it? */
    if (elem.nextSibling)
        /*
         * Is the other element a <span> element with a class name of
         * /validationError/?
         */
        if (elem.nextSibling.nodeName == 'SPAN' &&
                Element.hasClassName(elem.nextSibling, 'validationError'))
            $(p_id).parentNode.removeChild(elem.nextSibling);
}

This method of validation could also handle form submissions on the fly, but the developer would have to have a very specific need to do this.

Once the server comes back with a response, the method must alert the user if there is a problem without interrupting what she is doing. The easiest way to do this is to create an element and place it next to the input that has the problem, as Example 15-5 showed.

Client and Server Checking in One

By using Ajax to aid in validation, a developer can have the power of client and server validation on a field before a form is ever submitted to the server. This can be a powerful tool, but it is not a replacement for server validation on a complete form submission. This is because there is still no way to be sure where a form submission came from, and the server should not take any unnecessary risks. The point of using Ajax in this manner is to speed up the application by trying to ensure that the data is good before the server ever tries to do anything with it. Of course, the server takes a stab at all of the data in the first place, because it must assume that no data submitted by a client has already been validated.

This can come in handy in several places, as you will see in the next chapter. Keep in mind that there is always a price when using Ajax, and the developer must weigh whether any given form really needs such advanced validation functionality.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset