AJAX Form Validation
Validating input data is an essential requirement for quality and secure software applications. In the case of web applications, validation is an even more sensitive area, because your application is widely reachable by many users with varying skill sets and intentions.
Validation is not something to play with, because invalid data has the potential to harm the application's functionality, and even corrupt the application's most sensitive area: the database.
Input data validation means checking whether the data entered by the user complies with previously defined rules, which are established according to the business rules of your application. For example, if you require dates to be entered in the YYYY-MM-DD format, then a date of "February 28" would be considered invalid. Email addresses and phone numbers are other examples of data that should be checked against valid formats.
Carefully define the input data validation rules in the software requirements document of the application you're developing, and then use them consistently to validate your data!
Historically, web form validation was implemented mostly at the server side, after the form was submitted. In some cases, there was also some JavaScript code on the client that performed simple validation such as checking whether the email address was valid, or if a user name had been entered.
The problems encountered with traditional web form validation techniques are:
• Server-side form validation meets the limits of the HTTP protocol, which is a stateless protocol. Unless special code is written to deal with this issue, after submitting a page containing invalid data, the user is shown back an empty form that has to be filled from scratch.
• When submitting the page, the user needs to wait for a full page reload. For every mistake that is made when filling the form, a new page reload happens.
In this chapter, we will create a form-validation application that implements the good old traditional techniques and adds an AJAX flavor, thereby making the form more user-friendly and responsive.
Even if you implement AJAX validation, server-side validation is mandatory, because the server is the last line of defense against invalid data. The JavaScript code that gets to the client can not only be disabled permanently from the browser's settings, but it also can be easily modified or bypassed.
The code in this chapter can be verified online at http://ajaxphp.packtpub.com.
Implementing AJAX Form Validation
The form-validation application we will build in this chapter validates the form at the server side
on the classic form submit, and also implements AJAX validation while the user navigates through the form. The final validation is performed at the server, as shown in Figure 4.1.
Figure 4.1: Validation Being Performed Seamlessly while Users Continue Their Activity
Doing a final server-side validation when the form is submitted is always a must. If someone disables JavaScript in the browser settings, AJAX validation on the client side won't work, exposing sensitive data, and thereby allowing an evil-intended visitor to harm important data back on the server (e.g. through SQL injection).
Always validate user input on the server.
The application you are about to build validates a registration form, as shown in Figure 4.2, using both AJAX validation (client side) and typical server-side validation:
• AJAX-style—when each form field loses focus (onblur). The field's value is sent to the server, which validates the data and returns a result (0 for failure, 1 for success). If validation fails, an error message will unobtrusively show up and notify the user about the failed validation as shown in Figure 4.3.
• PHP-style—when the entire form is submitted. This is the usual validation you would do on the server, by checking user input against certain rules. If no errors are found and the input data is valid, the browser is redirected to a success page as shown in Figure 4.4. If validation fails, however, the user is sent back to the form page with the invalid fields highlighted as shown in Figure 4.3.
Both AJAX validation and PHP validation check the entered data against these rules:
• Username must not already exist in the database
• Name field cannot be empty
• A gender must be selected
• Month of Birth must be selected
• Birthday must be a valid date (between 1-31)
• Year of birth must be a valid year (between 1900-2000)
• The date must exist taking into consideration the number of days for each month
• Email address must be written in a valid email format, such as filip@yahoo.co.uk or cristian@subdomain.domain.com
• Phone number must be written in standard US form: xxx-xxx-xxxx
• "I've read the Terms of Use" must be checked
Watch the application in action in the following screenshots:
Figure 4.2: The User Registration Form
Figure 4.3: Form Validation in Action
Figure 4.4: Successful Submission
Thread-Safe AJAX
A piece of code is thread-safe if it functions correctly during simultaneous execution by multiple threads. This chapter contains the first example where an external factor—the user—directly influences the AJAX requests. We need to make an asynchronous request to the server to validate the entered data every time the user leaves an input box or changes a selection.
The hidden danger behind this technique is only revealed if the user moves very quickly through the input fields, or the server connection is slow; in these cases, the web application would attempt to make new server requests through an XMLHttpRequest object that is still busy waiting for the response to a previous request (this would generate an error and the application would stop functioning properly).
Depending on the circumstances at hand, the ideal solution to this problem may be:
• Create a new XMLHttpRequest instance for every message you need to send to the server. This method is easy to implement, but it can degrade server's performance if multiple requests are sent at the same time, and it doesn't guarantee for the order in which you receive the responses.
• Record the message in a queue and send it later when the XMLHttpRequest object is able to make new requests. The requests are made in the expected order. Using a queue is particularly important in applications where the order of the messages is important.
• Schedule to automatically retry making the request after a specified amount of time.
This method is similar to the one with the queue in that you don't make more than one server request at a time, but it doesn't guarantee for either the order in which the requests are made, or for the order in which the responses are received.
• Ignore the message.
In this chapter, for the first time in the book, we'll choose to implement a message queue. When the user leaves an input element, a message to validate its value is added to the queue. When the XMLHttpRequest object is clear to make a new request, it takes the first message from the queue.
The queue is a First-In, First-Out (FIFO) structure, which guarantees that the messages are sent in the proper order. To get a feeling about how this works, go to the demo page for this chapter (or implement the code), and press tab quickly multiple times, and then wait to see how the validation responses show up one by one.
Note that dealing with these problems only makes sense in scenarios where elements outside your control can trigger the server requests. Otherwise, in scenarios such as the Friendly application from Chapter 3, where you initiated new requests only after the response was received, implementing thread-safe code doesn't make a huge difference.
It's time to code.
Time for Action—AJAX Form Validation
If you have read the previous chapter then you should already have the users table set up. If you do, you may skip steps 1 and 2.
1. Connect to the ajax database, and create a table named users with the following code:
CREATE TABLE users
(
user_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
user_name VARCHAR(32) NOT NULL, PRIMARY KEY (user_id)
);
2. Execute the following INSERT commands to populate your users table with some sample data (because user_id is an auto_increment column, its values will be generated by the database):
INSERT INTO users (user_name) VALUES ('bogdan'); INSERT INTO users (user_name) VALUES ('filip'); INSERT INTO users (user_name) VALUES ('mihai'); INSERT INTO users (user_name) VALUES ('emilian'); INSERT INTO users (user_name) VALUES ('paula'); INSERT INTO users (user_name) VALUES ('cristian');
3. In your ajax folder, create a new folder named validate.
4. Let's start writing the code with the presentation tier. Create a file named
validate.css, and add the following code to it:
body
{
font-family: Arial, Helvetica, sans-serif;
font-size: 0.8em;
color: #000000;
}
label
{
float: left;
width: 150px;
font-weight: bold;
}
input, select
{
margin-bottom: 3px;
}
.button
{
font-size: 2em;
}
.left
{
margin-left: 150px;
}
.txtFormLegend
{
color: #777777;
font-weight: bold;
font-size: large;
}
.txtSmall
{
color: #999999;
font-size: smaller;
}
.hidden
{
display: none;
}
.error
{
display: block;
margin-left: 150px;
color: #ff0000;
}
5. Now create a new file named index_top.php, and add the following code. This script will be loaded from the main page index.php.
<?php
// enable PHP session session_start();
// Build HTML <option> tags
function buildOptions($options, $selectedOption)
{
foreach ($options as $value => $text)
{
if ($value == $selectedOption)
{
echo '<option value="' . $value .
'" selected="selected">' . $text . '</option>';
}
else
{
echo '<option value="' . $value . '">' . $text . '</option>';
}
}
}
// initialize gender options array
$genderOptions = array("0" => "[Select]", "1" => "Male",
"2" => "Female");
// initialize month options array
$monthOptions = array("0" => "[Select]", "1" => "January", "2" => "February",
"3" => "March", "4" => "April", "5" => "May", "6" => "June", "7" => "July",
"8" => "August",
"9" => "September", "10" => "October",
"11" => "November", "12" => "December");
// initialize some session variables to prevent PHP throwing Notices if (!isset($_SESSION['values']))
{
$_SESSION['values']['txtUsername'] = '';
$_SESSION['values']['txtName'] = '';
$_SESSION['values']['selGender'] = '';
$_SESSION['values']['selBthMonth'] = '';
$_SESSION['values']['txtBthDay'] = '';
$_SESSION['values']['txtBthYear'] = '';
$_SESSION['values']['txtEmail'] = '';
$_SESSION['values']['txtPhone'] = '';
$_SESSION['values']['chkReadTerms'] = '';
}
if (!isset($_SESSION['errors']))
{
$_SESSION['errors']['txtUsername'] = 'hidden';
$_SESSION['errors']['txtName'] = 'hidden';
$_SESSION['errors']['selGender'] = 'hidden';
$_SESSION['errors']['selBthMonth'] = 'hidden';
$_SESSION['errors']['txtBthDay'] = 'hidden';
$_SESSION['errors']['txtBthYear'] = 'hidden';
$_SESSION['errors']['txtEmail'] = 'hidden';
$_SESSION['errors']['txtPhone'] = 'hidden';
$_SESSION['errors']['chkReadTerms'] = 'hidden';
}
?>
6. Now create index.php, and add this code to it:
<?php
require_once ('index_top.php');
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Practical AJAX: Form Validation</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link href="validate.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="validate.js"></script>
</head>
<body onload="setFocus();">
<fieldset>
<legend>New User Registration Form</legend>
<br />
<form name="frmRegistration" method="post" action="validate.php?validationType=php">
<!-- Username -->
<label for="txtUsername">Desired username:</label>
<input id="txtUsername" name="txtUsername" type="text"
onblur="validate(this.value, this.id)"
value="<?php echo $_SESSION['values']['txtUsername'] ?>" />
<span id="txtUsernameFailed"
class="<?php echo $_SESSION['errors']['txtUsername'] ?>"> This username is in use, or empty username field.
</span>
<br />
<!-- Name -->
<label for="txtName">Your name:</label>
<input id="txtName" name="txtName" type="text" onblur="validate(this.value, this.id)"
value="<?php echo $_SESSION['values']['txtName'] ?>" />
<span id="txtNameFailed"
class="<?php echo $_SESSION['errors']['txtName'] ?>">
Please enter your name.
</span>
<br />
<!-- Gender -->
<label for="selGender">Gender:</label>
<select name="selGender" id="selGender" onblur="validate(this.value, this.id)">
<?php buildOptions($genderOptions,
$_SESSION['values']['selGender']); ?>
</select>
<span id="selGenderFailed"
class="<?php echo $_SESSION['errors']['selGender'] ?>"> Please select your gender.
</span>
<br />
<!-- Birthday -->
<label for="selBthMonth">Birthday:</label>
<!-- Month -->
<select name="selBthMonth" id="selBthMonth" onblur="validate(this.value, this.id)">
<?php buildOptions($monthOptions,
$_SESSION['values']['selBthMonth']); ?>
</select>
-
<!-- Day -->
<input type="text" name="txtBthDay" id="txtBthDay" maxlength="2" size="2"
onblur="validate(this.value, this.id)"
value="<?php echo $_SESSION['values']['txtBthDay'] ?>" />
-
<!-- Year -->
<input type="text" name="txtBthYear" id="txtBthYear" maxlength="4" size="2"
onblur="validate(document.getElementById('selBthMonth').options[document.g etElementById('selBthMonth').selectedIndex].value + '#' + document.getElementById('txtBthDay').value + '#' + this.value, this.id)"
value="<?php echo $_SESSION['values']['txtBthYear'] ?>" />
<!-- Month, Day, Year validation -->
<span id="selBthMonthFailed"
class="<?php echo $_SESSION['errors']['selBthMonth'] ?>"> Please select your birth month.
</span>
<span id="txtBthDayFailed"
class="<?php echo $_SESSION['errors']['txtBthDay'] ?>">
Please enter your birth day.
</span>
<span id="txtBthYearFailed"
class="<?php echo $_SESSION['errors']['txtBthYear'] ?>"> Please enter a valid date.
</span>
<br />
<!-- Email -->
<label for="txtEmail">E-mail:</label>
<input id="txtEmail" name="txtEmail" type="text"
onblur="validate(this.value, this.id)"
value="<?php echo $_SESSION['values']['txtEmail'] ?>" />
<span id="txtEmailFailed"
class="<?php echo $_SESSION['errors']['txtEmail'] ?>"> Invalid e-mail address.
</span>
<br />
<!-- Phone number -->
<label for="txtPhone">Phone number:</label>
<input id="txtPhone" name="txtPhone" type="text" onblur="validate(this.value, this.id)"
value="<?php echo $_SESSION['values']['txtPhone'] ?>" />
<span id="txtPhoneFailed"
class="<?php echo $_SESSION['errors']['txtPhone'] ?>">
Please insert a valid US phone number (xxx-xxx-xxxx).
</span>
<br />
<!-- Read terms checkbox -->
<input type="checkbox" id="chkReadTerms" name="chkReadTerms"
class="left" onblur="validate(this.checked, this.id)"
<?php if ($_SESSION['values']['chkReadTerms'] == 'on')
echo 'checked="checked"' ?> /> I've read the Terms of Use
<span id="chkReadTermsFailed"
class="<?php echo $_SESSION['errors']['chkReadTerms'] ?>"> Please make sure you read the Terms of Use.
</span>
<!-- End of form -->
<hr />
<span>Note: All fields are required.</span>
<br /><br />
<input type="submit" name="submitbutton" value="Register"
class="left button" />
</form>
</fieldset>
</body>
</html>
7. Create a new file named allok.php, and add the following code to it:
<?php
// clear any data saved in the session session_start();
session_destroy();
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>AJAX Form Validation</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link href="validate.css" rel="stylesheet" type="text/css" />
</head>
<body>
Registration Successfull!<br />
<a href="index.php" title="Go back"><< Go back</a>
</body>
</html>
8. Create a file named validate.js. This file performs the client-side functionality, including the AJAX requests:
// holds an instance of XMLHttpRequest
var xmlHttp = createXmlHttpRequestObject();
// holds the remote server address
var serverAddress = "validate.php";
// when set to true, display detailed error messages var showErrors = true;
// initialize the validation requests cache var cache = new Array();
// creates an XMLHttpRequest instance function createXmlHttpRequestObject()
{
// will store the reference to the XMLHttpRequest object var xmlHttp;
// this should work for all browsers except IE6 and older
try
{
// try to create XMLHttpRequest object
xmlHttp = new XMLHttpRequest();
}
catch(e)
{
// assume IE6 or older
var XmlHttpVersions = new Array("MSXML2.XMLHTTP.6.0",
"MSXML2.XMLHTTP.5.0", "MSXML2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP");
// try every id until one works
for (var i=0; i<XmlHttpVersions.length && !xmlHttp; i++)
{
try
{
// try to create XMLHttpRequest object
xmlHttp = new ActiveXObject(XmlHttpVersions[i]);
}
catch (e) {} // ignore potential error
}
}
// return the created object or display an error message if (!xmlHttp)
displayError("Error creating the XMLHttpRequest object.");
else
return xmlHttp;
}
// function that displays an error message function displayError($message)
{
// ignore errors if showErrors is false
if (showErrors)
{
// turn error displaying Off
showErrors = false;
// display error message
alert("Error encountered: \n" + $message);
// retry validation after 10 seconds
setTimeout("validate();", 10000);
}
}
// the function handles the validation for any form field function validate(inputValue, fieldID)
{
// only continue if xmlHttp isn't void if (xmlHttp)
{
// if we received non-null parameters, we add them to cache in the
// form of the query string to be sent to the server for validation
if (fieldID)
{
// encode values for safely adding them to an HTTP request query string
inputValue = encodeURIComponent(inputValue);
fieldID = encodeURIComponent(fieldID);
// add the values to the queue
cache.push("inputValue=" + inputValue + "&fieldID=" + fieldID);
}
// try to connect to the server
try
{
// continue only if the XMLHttpRequest object isn't busy
// and the cache is not empty
if ((xmlHttp.readyState == 4 || xmlHttp.readyState == 0)
&& cache.length > 0)
{
// get a new set of parameters from the cache var cacheEntry = cache.shift();
// make a server request to validate the extracted data xmlHttp.open("POST", serverAddress, true); xmlHttp.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded"); xmlHttp.onreadystatechange = handleRequestStateChange; xmlHttp.send(cacheEntry);
}
}
catch (e)
{
// display an error when failing to connect to the server displayError(e.toString());
}
}
}
// function that handles the HTTP response function handleRequestStateChange()
{
// when readyState is 4, we read the server response if (xmlHttp.readyState == 4)
{
// continue only if HTTP status is "OK" if (xmlHttp.status == 200)
{
try
{
// read the response from the server readResponse();
}
catch(e)
{
// display error message
displayError(e.toString());
}
}
else
{
// display error message
displayError(xmlHttp.statusText);
}
}
}
// read server's response function readResponse()
{
// retrieve the server's response
var response = xmlHttp.responseText;
// server error?
if (response.indexOf("ERRNO") >= 0
|| response.indexOf("error:") >= 0
|| response.length == 0)
throw(response.length == 0 ? "Server error." : response);
// get response in XML format (assume the response is valid XML)
responseXml = xmlHttp.responseXML;
// get the document element
xmlDoc = responseXml.documentElement;
result = xmlDoc.getElementsByTagName("result")[0].firstChild.data;
fieldID = xmlDoc.getElementsByTagName("fieldid")[0].firstChild.data;
// find the HTML element that displays the error message = document.getElementById(fieldID + "Failed");
// show the error or hide the error
message.className = (result == "0") ? "error" : "hidden";
// call validate() again, in case there are values left in the cache setTimeout("validate();", 500);
}
// sets focus on the first field of the form function setFocus()
{
document.getElementById("txtUsername").focus();
}
9. It's time to add the business logic now. Start by creating config.php, with this code in it:
<?php
// defines database connection data
define('DB_HOST', 'localhost'); define('DB_USER', 'ajaxuser'); define('DB_PASSWORD', 'practical'); define('DB_DATABASE', 'ajax');
?>
10. Now create the error handler code in a file named error_handler.php:
<?php
// set the user error handler method to be error_handler
set_error_handler('error_handler', E_ALL);
// error handler function
function error_handler($errNo, $errStr, $errFile, $errLine)
{
// clear any output that has already been generated
if(ob_get_length()) ob_clean();
// output the error message
$error_message = 'ERRNO: ' . $errNo . chr(10) .
'TEXT: ' . $errStr . chr(10) .
'LOCATION: ' . $errFile .
', line ' . $errLine;
echo $error_message;
// prevent processing any more PHP scripts exit;
}
?>
11. The PHP script that handles the client's AJAX calls, and also handles the validation on form submit, is validate.php:
<?php
// start PHP session session_start();
// load error handling script and validation class
require_once ('error_handler.php');
require_once ('validate.class.php');
// Create new validator object
$validator = new Validate();
// read validation type (PHP or AJAX?)
$validationType = '';
if (isset($_GET['validationType']))
{
$validationType = $_GET['validationType'];
}
// AJAX validation or PHP validation?
if ($validationType == 'php')
{
// PHP validation is performed by the ValidatePHP method, which returns
// the page the visitor should be redirected to (which is allok.php if
// all the data is valid, or back to index.php if not)
header("Location:" . $validator->ValidatePHP());
}
else
{
// AJAX validation is performed by the ValidateAJAX method. The results
// are used to form an XML document that is sent back to the client
$response =
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' .
'<response>' .
'<result>' .
$validator->ValidateAJAX($_POST['inputValue'], $_POST['fieldID']) .
'</result>' .
'<fieldid>' .
$_POST['fieldID'] .
'</fieldid>' .
'</response>';
// generate the response
if(ob_get_length()) ob_clean(); header('Content-Type: text/xml'); echo $response;
}
?>
12. The class that supports the validation functionality is called Validate, and it is hosted in a script file called validate.class.php, which looks like this:
<?php
// load error handler and database configuration
require_once ('config.php');
// Class supports AJAX and PHP web form validation class Validate
{
// stored database connection private $mMysqli;
// constructor opens database connection function __construct()
{
$this->mMysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE);
}
// destructor closes database connection function __destruct()
{
$this->mMysqli->close();
}
// supports AJAX validation, verifies a single value public function ValidateAJAX($inputValue, $fieldID)
{
// check which field is being validated and perform validation switch($fieldID)
{
// Check if the username is valid case 'txtUsername':
return $this->validateUserName($inputValue);
break;
// Check if the name is valid case 'txtName':
return $this->validateName($inputValue);
break;
// Check if a gender was selected case 'selGender':
return $this->validateGender($inputValue);
break;
// Check if birth month is valid case 'selBthMonth':
return $this->validateBirthMonth($inputValue);
break;
// Check if birth day is valid case 'txtBthDay':
return $this->validateBirthDay($inputValue);
break;
// Check if birth year is valid case 'txtBthYear':
return $this->validateBirthYear($inputValue);
break;
// Check if email is valid case 'txtEmail':
return $this->validateEmail($inputValue);
break;
// Check if phone is valid case 'txtPhone':
return $this->validatePhone($inputValue);
break;
// Check if "I have read the terms" checkbox has been checked case 'chkReadTerms':
return $this->validateReadTerms($inputValue);
break;
}
}
// validates all form fields on form submit public function ValidatePHP()
{
// error flag, becomes 1 when errors are found.
$errorsExist = 0;
// clears the errors session flag if (isset($_SESSION['errors']))
unset($_SESSION['errors']);
// By default all fields are considered valid
$_SESSION['errors']['txtUsername'] = 'hidden';
$_SESSION['errors']['txtName'] = 'hidden';
$_SESSION['errors']['selGender'] = 'hidden';
$_SESSION['errors']['selBthMonth'] = 'hidden';
$_SESSION['errors']['txtBthDay'] = 'hidden';
$_SESSION['errors']['txtBthYear'] = 'hidden';
$_SESSION['errors']['txtEmail'] = 'hidden';
$_SESSION['errors']['txtPhone'] = 'hidden';
$_SESSION['errors']['chkReadTerms'] = 'hidden';
// Validate username
if (!$this->validateUserName($_POST['txtUsername']))
{
$_SESSION['errors']['txtUsername'] = 'error';
$errorsExist = 1;
}
// Validate name
if (!$this->validateName($_POST['txtName']))
{
$_SESSION['errors']['txtName'] = 'error';
$errorsExist = 1;
}
// Validate gender
if (!$this->validateGender($_POST['selGender']))
{
$_SESSION['errors']['selGender'] = 'error';
$errorsExist = 1;
}
// Validate birth month
if (!$this->validateBirthMonth($_POST['selBthMonth']))
{
$_SESSION['errors']['selBthMonth'] = 'error';
$errorsExist = 1;
}
// Validate birth day
if (!$this->validateBirthDay($_POST['txtBthDay']))
{
$_SESSION['errors']['txtBthDay'] = 'error';
$errorsExist = 1;
}
// Validate birth year and date
if (!$this->validateBirthYear($_POST['selBthMonth'] . '#' .
$_POST['txtBthDay'] . '#' .
$_POST['txtBthYear']))
{
$_SESSION['errors']['txtBthYear'] = 'error';
$errorsExist = 1;
}
// Validate email
if (!$this->validateEmail($_POST['txtEmail']))
{
$_SESSION['errors']['txtEmail'] = 'error';
$errorsExist = 1;
}
// Validate phone
if (!$this->validatePhone($_POST['txtPhone']))
{
$_SESSION['errors']['txtPhone'] = 'error';
$errorsExist = 1;
}
// Validate read terms
if (!isset($_POST['chkReadTerms']) ||
!$this->validateReadTerms($_POST['chkReadTerms']))
{
$_SESSION['errors']['chkReadTerms'] = 'error';
$_SESSION['values']['chkReadTerms'] = '';
$errorsExist = 1;
}
// If no errors are found, point to a successful validation page if ($errorsExist == 0)
{
return 'allok.php';
}
else
{
// If errors are found, save current user input
foreach ($_POST as $key => $value)
{
$_SESSION['values'][$key] = $_POST[$key];
}
return 'index.php';
}
}
// validate user name (must be empty, and must not be already registered)
private function validateUserName($value)
{
// trim and escape input value
$value = $this->mMysqli->real_escape_string(trim($value));
// empty user name is not valid if ($value == null)
return 0; // not valid
// check if the username exists in the database
$query = $this->mMysqli->query('SELECT user_name FROM users ' .
'WHERE user_name="' . $value . '"');
if ($this->mMysqli->affected_rows > 0)
return '0'; // not valid
else
return '1'; // valid
}
// validate name
private function validateName($value)
{
// trim and escape input value
$value = trim($value);
// empty user name is not valid
if ($value)
return 1; // valid else
return 0; // not valid
}
// validate gender
private function validateGender($value)
{
// user must have a gender return ($value == '0') ? 0 : 1;
}
// validate birth month
private function validateBirthMonth($value)
{
// month must be non-null, and between 1 and 12
return ($value == '' || $value > 12 || $value < 1) ? 0 : 1;
}
// validate birth day
private function validateBirthDay($value)
{
// day must be non-null, and between 1 and 31
return ($value == '' || $value > 31 || $value < 1) ? 0 : 1;
}
// validate birth year and the whole date private function validateBirthYear($value)
{
// valid birth year is between 1900 and 2000
// get whole date (mm#dd#yyyy)
$date = explode('#', $value);
// date can't be valid if there is no day, month, or year
if (!$date[0]) return 0;
if (!$date[1] || !is_numeric($date[1])) return 0;
if (!$date[2] || !is_numeric($date[2])) return 0;
// check the date
return (checkdate($date[0], $date[1], $date[2])) ? 1 : 0;
}
// validate email
private function validateEmail($value)
{
// valid email formats: *@*.*, *@*.*.*, *.*@*.*, *.*@*.*.*)
return (!eregi('^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-
]+)*(\.[a-z]{2,3})$', $value)) ? 0 : 1;
}
// validate phone
private function validatePhone($value)
{
// valid phone format: ###-###-####
return (!eregi('^[0-9]{3}-*[0-9]{3}-*[0-9]{4}$', $value)) ? 0 : 1;
}
// check the user has read the terms of use private function validateReadTerms($value)
{
// valid value is 'true'
return ($value == 'true' || $value == 'on') ? 1 : 0;
}
}
?>
13. Test your script by loading http://localhost/ajax/validate/index.php in a web browser.
What Just Happened?
The AJAX validation technique allows us to validate form fields and at the same time inform users if there were any validation errors. But the cherry on the top of the cake is that we are doing all of this without interrupting the user's activity! This is called unobtrusive form validation.
The unobtrusive validation is combined with a pure server-side PHP validation that happens when submitting the form. At the server, both validation types are supported by a PHP script called validate.php, with the help of another PHP script called validate.class.php.
Let us examine the code, starting with the script that handles client-side validation, index.php. In this validation example, the client page is not a simple HTML file, but a PHP file instead, so portions of it will be still dynamically generated at the server side. This is necessary because we want to retain the form field values when the form is submitted and server-side validation fails. Without
the help of the PHP code, when the index page is reloaded, all its fields would become empty.
index.php starts with loading a helper script named index_top.php, which starts the session by calling session_start(), defines some variables and a function that will be used later in index.php, and initializes some session variables ($_SESSION['values'] and $_SESSION['errors']) that we will be using to avoid PHP sending notices about variables that are not initialized.
Notice the onload event of the body tag in index.php. It calls the setFocus() function defined in
validate.js, which sets the input cursor on the first form field.
Later in index.php, you will see the following sequence of code repeating itself, with only small changes:
<!-- Username -->
<label for="txtUsername">Desired username:</label>
<input id="txtUsername" name="txtUsername" type="text"
onblur="validate(this.value, this.id)"
value="<?php echo $_SESSION['values']['txtUsername'] ?>" />
<span id="txtUsernameFailed"
class="<?php echo $_SESSION['errors']['txtUsername'] ?>"> This username is in use, or empty username field.
</span>
<br />
This is the code that displays a form field with its label and displays an error message underneath it if a validation has been performed and has failed.
In this example, we display an error message right under the validated field, but you can customize the position and appearance of these error messages in validate.css by changing the properties of the error CSS class.
The onblur event of the input element, which is generated when the user leaves an input element, triggers the validate() JavaScript function with two parameters: the field's value and the field's ID. This function will handle AJAX validation, by making an asynchronous HTTP request to the validate.php script. The server script needs to know which field we need to validate and what
the input value is.
The value attribute should be empty on first page load, but after submitting the form it will hold the input value, in case the form is reloaded as a result of a validation error. We use session variables to save user input on form submit, in case validation fails and the form is re-displayed.
The span element that follows contains an error message that gets displayed on failed validation. This span is initially hidden using the hidden CSS class, but we change its CSS class into error, if validation fails.
Inside validate.js, the validate function sends an AJAX request to the server, by calling
validate.php with two parameters, the field's value and the field's ID.
Remember that XMLHttpRequest cannot make two HTTP requests at the same time, so if the object is busy processing a previous request, we save the details of the current request for later. This is particularly useful when the connection to the network or the Internet is slow. The request details are saved using a cache system with the properties of a FIFO structure. Luckily, the JavaScript's Array class offers the exact functionality we need (through its push and shift methods) and hence we use it for caching purposes:
var cache = new Array();
So validate() starts by adding the data to validate to the cache (if the function received any).
// the function handles the validation for any form field function validate(inputValue, fieldID)
{
// only continue if xmlHttp isn't void
if (xmlHttp)
{
// if we received non-null parameters, we add them to cache
// in the form of the query string to be sent to the server for validation if (fieldID)
{
// encode values for safely adding them to an HTTP request query string inputValue = encodeURIComponent(inputValue);
fieldID = encodeURIComponent(fieldID);
// add the values to the queue
cache.push("inputValue=" + inputValue + "&fieldID=" + fieldID);
}
This adds a new element at the end of our cache array. The cache entry is composed of two parts, the value and the ID of the field to be validated, separated by '&'. Note that the new element is added only if fieldID is not null. The value of fieldID is null when the function is called just to check if the cache contains any pending validations to be made, without adding new entries to the cache.
The field ID and value retrieved from the cache will be sent to the server for validation. To make sure they arrive at the destination successfully and unaltered, they are escaped using JavaScript's encodeURIComponent function. This enables safely transmitting any characters to the server, including characters such as "&" which otherwise would cause problems. For more details, please read an excellent article on JavaScript's escaping functions at http://xkr.us/articles/javascript/encode-compare/.
If the XMLHttpRequest object is free to initiate new HTTP requests, we use shift() to get a new value from the cache to validate (this function also removes the entry from the cache array, which is perfect for our purposes). Note that this value may not be the one just added using push—in FIFO scenarios, the oldest (pending) record is retrieved first.
// try to connect to the server try
{
// continue only if the XMLHttpRequest object isn't busy
// and the cache is not empty
if ((xmlHttp.readyState == 4 || xmlHttp.readyState == 0)
&& cache.length>0)
{
//
var cacheEntry = cache.shift();
If the XMLHttpRequest object's status is 0 or 4 it means that there are no active requests and we can send a new request. When sending the new request, we use the data read from the cache, which already contains the formatted query string:
// make a server request to validate the extracted data xmlHttp.open("POST", serverAddress, true); xmlHttp.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded"); xmlHttp.onreadystatechange = handleRequestStateChange; xmlHttp.send(cacheEntry);
}
The function that handles the server's response is called handleRequestStateChange, and in turn calls readResponse() once the response is fully received from the server. This method starts by checking if what we received from the server is the report of a server-side error:
// read server's response function readResponse()
{
// retrieve the server's response
var response = xmlHttp.responseText;
// server error?
if (response.indexOf("ERRNO") >= 0
|| response.indexOf("error:") >= 0
|| response.length == 0)
throw(response.length == 0 ? "Server error." : response);
After this basic check is done, we read the server's response, which tells us if the value is valid or not:
// get response in XML format (assume the response is valid XML)
responseXml = xmlHttp.responseXML;
// get the document element
xmlDoc = responseXml.documentElement;
result = xmlDoc.getElementsByTagName("result")[0].firstChild.data;
fieldID = xmlDoc.getElementsByTagName("fieldid")[0].firstChild.data;
Depending on the result, we change the CSS class of the error message associated with the tested element to hidden (if the validation was successful), or to error (if the validation failed). You change the element's CSS class using its className property:
// find the HTML element that displays the error message = document.getElementById(fieldID + "Failed");
// show the error or hide the error
message.className = (result == "0") ? "error" : "hidden";
// call validate() again, in case there are values left in the cache
setTimeout("validate();", 500);
}
The PHP script that handles server-side processing is validate.php. It starts by loading the error handling script (error_handler.php) and the Validate class that handles data validation (validate.class.php). Then, it looks for a GET variable named validationType. This only exists when the form is submitted, as the form's action attribute is validate.php?validationType=php.
// read validation type (PHP or AJAX?)
$validationType = '';
if (isset($_GET['validationType']))
{
$validationType = $_GET['validationType'];
}
Then, based on the value of $validationType, we perform either AJAX validation or PHP validation.
// AJAX validation or PHP validation?
if ($validationType == 'php')
{
// PHP validation is performed by the ValidatePHP method, which returns
// the page the visitor should be redirected to (which is allok.php if
// all the data is valid, or back to index.php if not)
header("Location:" . $validator->ValidatePHP());
}
else
{
// AJAX validation is performed by the ValidateAJAX method. The results
// are used to form an XML document that is sent back to the client
$response =
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' .
'<response>' .
'<result>' .
$validator->ValidateAJAX($_POST['inputValue'], $_POST['fieldID']) .
'</result>' .
'<fieldid>' .
$_POST['fieldID'] .
'</fieldid>' .
'</response>';
// generate the response if(ob_get_length()) ob_clean();
header('Content-Type: text/xml');
echo $response;
}
?>
If we are dealing with classic server-side validation, we call the validatePHP() method, which returns the name of the page the browser should be redirected to (which will be allok.php if the validation was successful, or index.php if not). The validation results for each field are stored in the session and if it gets reloaded, index.php will show the fields that didn't pass the test.
In the case of AJAX calls, the server composes a response that specifies if the field is valid. The response is a short XML document that looks like this:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<response>
<result>0</result>
<fieldid>txtUsername</fieldid>
</response>
If the result is 0, then txtUsername isn't valid and should be marked accordingly. If the result is 1, the field's value is valid.
Next, let's look into validate.class.php. The class constructor creates a connection to the database and the destructor closes that connection. We then have two public methods: ValidateAJAX (handles AJAX validation) and ValidatePHP (handles typical server-side validation).
AJAX validation requires two parameters, one that holds the value to be validated ($inputValue) and one that holds the form field's ID ($fieldID). A switch block loads specific validation for each form field. This function will return 0 if validation fails or 1 if validation is successful.
The PHP validation function takes no parameters, as it will always validate the entire form (after form submit). First we initialize the $errorsExist flag to 0. Whenever validation fails for a field, this flag will be set to 1 and we will know validation has failed. Then we need to make sure that older session variables are unset in order to ensure that older errors are cleared.
We then check each form field against a set of custom-created rules. If validation fails, we raise the flag ($errorsExist = 1) and set the session variable that sets the CSS class for error message to error. If, in the end, the $errorsExist flag is still set to 0, it means that the whole validation has been successful and we return the name of the success page, thus redirecting the browser to that page.
If errors are found, we save current user input into session variables, which will be used by index.php to fill the form (remember that by default, when loading the page, all fields are empty). This is how we save current user input:
foreach ($_POST as $key => $value)
{
$_SESSION['values'][$key] = $_POST[$key];
}
$_POST is an array holding the names and values of all form elements, and it can be walked through with foreach. This means that for each element inside the $_POST array, we create a new element inside the $_SESSION['values'] array.
There's nothing special to mention about validate.css. The success page (allok.php) is very simple as well—it just displays a successful submission confirmation.
Summary
While we don't claim to have built the perfect validation technique, we provided a working proof of concept; a working application that takes care of user input and ensures its validity.
You cannot do that only with JavaScript nor would you want to wait for the field to be validated only on form submit.
The reason we used AJAX for pseudo client-side validation instead of simple JavaScript validation is that in many scenarios form fields need to be checked against a database (like the username field in this case). Also, in most cases it's more professional to have all the business logic (including the validation) stored in a central place on the server.
AJAX can be so handy, don't you think?
Tuesday, July 14, 2009
AJAX and PHP Building Responsive Web Applications by Cristian Darie, Bogdan Brinzarea Chapter 4
Posted by Mr Procces at 10:29 AM
Labels: AJAX, PHP, Web Applications, Web Development
Subscribe to:
Post Comments (Atom)
0 comments:
Post a Comment