User-Friendly Form Validation with PHP and CSS
Pages: 1, 2
A Sample Page
Here's a sample page called email.php that demonstrates the basic technique.
This page allows the user to enter an email address and name, such as in a signup page for a mailing list:
<?
$message = "";
$emailclass = "basictext";
$username = "";
if ($_POST['process'] == 1) {
$pattern = '/.*@.*\..*/';
$email = $_POST['email'];
$urlname = urlencode($$_POST['username']);
if (preg_match($pattern, $_POST['email']) > 0) {
// Here's where you would store
// the data in a database...
header(
"location: thankyou.php?&username=$urlname");
}
$message = "Please enter a valid email address.";
$username = $_POST['name'];
$emailclass = "errortext";
}
?>
<html>
<style>
.basictext {
font-family: Arial, Helvetica, sans-serif;
font-size: 14px; color:#000066;
}
.errortext {
font-family: Arial, Helvetica, sans-serif;
font-size: 14px; color:#C00000; font-weight: bold;
}
</style>
<body>
<form action="email.php" method="post">
<? if ($message != "") {
print '<span class="errortext">'.
$message."<span><br>\n";
}
?>
<span class="<?print $emailclass; ?>">
Email address:</span>
<input name="email" type="text"
class="<? print $emailclass; ?>"><br>
<span class="basictext">Your name:</span>
<input name="name" type="text" class="basictext"
value="<?print $username; ?>"><br>
<input type="hidden" name="process" value="1">
<input type="submit" name="Button1" value="Sign up!">
</form>
</body></html>
How it Works
The first section is the processing section. Notice the trick: if the
$_POST array contains a key called Process with a
value of 1, then begin processing; otherwise, just continue on.
In other words, the first time the user opens the page, processing will not
take place. You can see farther down that the form includes a hidden field
called process with the value 1. Thus, when the user
clicks the submit button, the page will load again, but this time processing
will take place. Voila! The problem of the chicken and egg has been solved!
(Note, however, that the purists might balk at the fact that I'm playing on
a feature in PHP: ff the $_POST['process'] variable doesn't exist,
as is the case the first time the page loads, PHP still lets me test it against
the value of 1; I'll simply get back a false. If this bothers you,
you can add an isset call to first check whether
$_POST['process'] even exists.)
The processing here is simple. I threw together a simple regular expression
to check for an email address. (However, please don't use this regular
expression in your own production code, as it's not foolproof; for a better
one, check out the PHP Cookbook.) If the email address matches the
regular expression, then all is fine and I issue a redirect to the next page,
called thankyou.php. If the address doesn't survive the regular
expression test, then I stay on the page and store a value in the
$message variable.
Next, notice how I play with the styles: I have two styles defined inside of the
<head> tag. One is for the basic text, a basic dark blue. The other is for
bold red text used with errors to attract attention to the problem. In the PHP
code, I store the basic text-style name inside of the $emailclass
variable. If the code encounters an invalid email address, it changes the value
of $emailclass to errortext. Then, when I create the
label for the email address and the text box for the email address, I use the
class stored in $emailclass, like so:
class="<?print $emailclass; ?>">
Thus, when all is fine, the HTML displays regular text. When there's a problem, it displays bold red text.
Notice also that I store some defaults in the fields. Initially,
$username holds an empty string. On the return trip through the
file, if the validation fails, I put the $_POST['name'] value back
into the $username variable. (Remember, and this is important: on
the return trip through the file, you are running the program as if it's brand
new. Any variables you set the previous time are no longer present, unless you
use a session variable; see Session handling functions for more information.) Later down in the code, I stuff the $username value into the form, like so:
<input name="name" type="text" class="basictext" value="<?print $username; ?>"><br>
Thus, if the user enters an improper email address, when the form returns,
it will have the username already filled in. This technique is extremely
important for user-friendly forms, because if the user forgets an
@ sign in the email address (or the AOL users just type a
screen name without @aol.com), he or she doesn't want to have to
retype everything else into the form. This way, the form will return already
populated with everything the user already typed in. In this case, I also
decided to fill in the email address box with what the user previously typed,
just so he or she can see the incorrect value and fix it.
The final thing to show you about this code is how I pass the value of the
username on to the next page. Notice first that the form uses a POST method.
Whenever you're sending out passwords, you want to use a POST method, since a
GET method will show the password in the URL (and in the history file). People
wouldn't be too happy about that if somebody is glancing over their shoulders.
Of course, I'm not using a password in this example, but you might be in your
own project; thus, I used the POST method as a demonstration.
However, the next page (in this case, thankyou.php) might well
need access to the data that the user entered. The easiest way to pass it is
through a GET, like so:
header("location: thankyou.php?&username=$urlname");
(I called urlencode
so that the spaces and any other unusual characters will make it across.) Yes,
I'm using a GET method, even though I just raved about the importance of using a
POST method. However, I didn't pass the email address. The reason is
that you would probably go ahead and store or process the username and password
right inside of the processing code before calling header, rather
than waiting until you get to the next page, in this case
thankyou.php. Why now? Because you're already doing all the verifying; you
might as well do the real processing, as well. For example, in a signup form,
you might need to test whether the chosen username already exists; if it does,
then you'll issue a message to the user that the username exists and to try a
different one. If all is fine, you could go ahead and store the information in
the database, before proceeding to the thankyou.php page. (It's
possible, then, that you won't have to pass anything at all to the
thankyou.php page, and therefore won't even need the
?&username= portion of the URL in the header call.)
More Possibilities
Here are some things that you could do with this technique:
Have the user enter an email address twice. In the processing, check if the two match, and if not, print a message stating that they don't match.
When a user signs up for a new account, test the chosen username and password against the database. If the username already exists, start a loop where you add on some random numbers to the username until you locate one that doesn't already exist in the database, and then suggest this username to the user.
Check if the user left any required fields blank. Add a message along the lines of, "Please fill in the missing fields," and note the missing fields with red labels, being sure to fill in the other fields with whatever the user previously typed in.
Make sure the password's length is within the required size. If it's less than eight characters, for example, put a message stating that the password must be at least eight characters.
You can even get fancier. When a user logs out, you could send the user back
to a login.php page, with an added message saying, "You have been
logged out. Use this form if you wish to log back in." How would you do that?
After logging the user out, redirect the user back to the
login.php page, passing a unique value (such as 2) in
with the URL, as in login.php?process=2. Then add an
if block to the login.php page checking the
$_GET['process'] for the value 2. If the comparison
succeeds, set the message appropriately. That way, you don't need a separate
login page just for the extra message. (You are free to mix $_GET
and $_POST all within the same page. PHP handles it just
fine.)
Conclusion
Using this technique, you can easily create some sophisticated web sites that don't require a lot of duplicate code and unnecessary processing, all while sticking to our beloved PHP without having to touch either JavaScript or ASP.
Jeff Cogswell lives near Cincinnati and has been working as a software engineer for over 15 years.
Return to the PHP DevCenter.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 16 of 16.
-
Expanding your example to multiple fields/types of fields
2006-04-06 14:56:13 aberns [Reply | View]
Hello,
I am very interested in server side form validation. With your solution however, I couldn't see how to extend it to validate more than just the email field. What if you have 10 text fields, one textarea, 8 select/option fields, 6 radio buttons, and 5 check boxes? Would you be willing to elaborate on how to check more than just one email field?
Out of desparation, I came up with an alternate solution, but I am almost too embarrased to post it, since I'm sure people will point out that it is horribly cumberson, inefficient, etc. I guess I will wait first for a response to my original question as to how to extend your solution to many different types of fields, to see if you are still monitoring this tutorial. Thank you very much.
Cheers,
Audrey
-
NEVER doing any client-side processing is a mistake
2004-09-25 04:06:11 TimP [Reply | View]
It's one thing to bounce a little form with a couple of fields back and forth between a browser and an idle web server. It's another to saddle a user with a screen full on a busy server. I've had that experience as a user, and if I was on a site that I could avoid, I'd just close the browser. As the interactive experience deteriorates because of the constant bouncing of a large complicated form, the slower the server gets. Very unfriendly and very bad for business.
While returning the same form with error messages right on it is the right (although well-known) approach, the friendliness is enhanced by cagey use of both client-side and server-side validation. We have code that keeps the submit buttons inoperative until all manditory fields are completed, the manditory fields are labeled in red as a visual clue, the field content errors are presented field-by-field or as a group depending on the flow of the form, and the server-side processing is limited to protecting the application and the database.
Granted, you'll have to stretch a bit technically if you don't regard JS to be as beloved as PHP. Try O'Reilly's "Javascript: the Definitive Guide". It's probably one of the top two or three books I've bought in the last 4 years. As the head carpenter send to his assistant: "Don't you be bangin' no screws in with that there hammer."
-
header redirection
2004-08-24 07:23:41 will_macdonald [Reply | View]
I very often find it useful to put exit; after a redirect command. I've had buggy code that sent TWO redirection commands to the browser. Got v confusing.
As mentioned in the PHP manual:
http://uk.php.net/header
header("Location: http://www.example.com/"); /* Redirect browser */
/* Make sure that code below does not get executed when we redirect. */
exit;
-
URL limitation
2004-05-26 21:47:08 adlinharis [Reply | View]
In the code, the value from the form is passed thru the URL but there are limitation on passing value on URL. So what is the way to pass the value to next page if the form contains lot of data??
-
$_SERVER['PHP_SELF']?
2004-05-18 15:03:57 tiffanyb [Reply | View]
You could also use action="$_SERVER['PHP_SELF']," and include the proper logic and the form in one page. -
$_SERVER['PHP_SELF']?
2009-05-17 21:56:17 Shubhamoy [Reply | View]
Hi,
We should avoid using $_SERVER['PHP_SELF'] instead htmlentities($_SERVER['PHP_SELF']) should be preferred. This will block the XSS Hack. For more information read this : http://www.html-form-guide.com/php-form/php-form-action-self.html
BR,
Shubhamoy
http://shubhamoy.com/blog
-
Minor point
2004-05-02 12:12:13 trb [Reply | View]
You said:
Because you're already doing all the verifying; you might as well do the real processing, as well.
You actually need to do all your processing on the same request cycle as the validation, otherwise it's trivial just to skip the validation altogether.
Remember, I'm free to spit bad data at any page you let me access directly, whether that be via form submission or redirect.
This should, of course, be common knowledge for anyone who's written more than a line or two of code for the web. Still, you never know who might be reading.
-
Short open tags
2004-04-27 16:46:47 moxicon [Reply | View]
Regarding hukka's comment about redirects, if the HTTP 1.1 spec says that the client should be prompted, what are we poor web application developers going to do?
I've worked with some developers who don't use redirects. They perform the form processing and response in one request/response cycle, with the rational being that the application runs faster (fewer requests/responses), but I've always kept with the redirects because it makes the application flow better from the user's perspective. The user never gets prompted to re-submit POST data when she hits the 'back' button. I haven't noticed much of a difference in speed.
However, there's still that darned HTTP spec. I read the spec for 302 and 307 codes, then I read the 303 spec, here. I believe the 303 code is there answer to that problem. Here's an example redirection that uses the 303 code:
<?php
header("HTTP/1.1 303 See Other");
header("Location: http://www.moxleydata.com/test_redirect_target.html");
?>
<html>
<head><title>Redirect</title></head>
<body>
Click here to continue.
</body>
</html>
If you have curl installed, run this command:
curl -i -X POST http://www.moxleydata.com/test_redirect.php
It outputs this:
HTTP/1.1 303 See Other
Date: Tue, 27 Apr 2004 17:25:48 GMT
Server: Apache/1.3.27 (Unix) (Red-Hat/Linux) FrontPage/5.0.2.2623 mod_python/2.7.8 Python/1.5.2 mod_ssl/2.8.12 OpenSSL/0.9.6b PHP/4.1.2 mod_perl/1.26 mod_webapp/1.2.0-dev
X-Powered-By: PHP/4.1.2
Location: http://www.moxleydata.com/test_display.html
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html
As the spec says, not all browsers understand the 303, so you might want to include browser detection so that your applications sends alternate headers for those older browsers.
Moxley Stratton
Moxley Data Systems
-
Short open tags
2004-04-29 08:56:09 Chris Shiflett |
[Reply | View]
You no longer have to use a separate header() call to set the response code for your redirect. We added a third, optional parameter to header() specifically for this purpose.
-
POST requests should not be redirected
2004-04-27 06:57:43 hukka [Reply | View]
Using header("Location: ...") on POST requests is troublesome because the HTTP spec requires the browser to explicitly ask for user confirmation for the redirect. From the HTTP 1.1 spec, section 10.3.3:
If the 302 status code is received in response to a request other than GET or HEAD, the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user, since this might change the conditions under which the request was issued.
This is hardly "user-friendly". Of course (as noted also in the spec) most current browsers (especially IE) do not require this confirmation, but one should not count on this behaviour. At least current Mozilla browsers comply with the standard.
Using the redirect trick is fine for GET requests. For web forms using POST, a better solution would be to output a status page showing a message about the entered information having been successfully processed. You can include the followup URL as a "continue" link on that status page, and you could even add a META refresh tag to have the browser automatically redirect to that address after a few seconds. -
POST requests should not be redirected
2004-04-29 09:13:48 Chris Shiflett |
[Reply | View]
That is how the specification is written, but it is not how it is applied in practice. Basically, 302 is treated exactly like the description of 303. In fact, 303 was added so that the ambiguity could be removed. If 303 was supported as consistently as 302 (e.g., by both old and new agents), we would probably return that instead of 302 when a developer sets a Location header.
-
Prevent cross site scripting vulnerability
2004-04-26 18:28:51 JHolmes763 [Reply | View]
Where you stick $username back into the text box, be sure to run it through htmlentities() first to prevent cross site scripting vulnerabilities.
<input type="text" name="username" value="<?php echo htmlentities($username); ?>">
Otherwise, someone can send a value starting with "> that'll end your input text box and they can inject HTML/JavaScript/whatever into the rest of the page.
---John Holmes...
-
Article Comments and Errata
2004-04-22 22:07:35 Chris Shiflett |
[Reply | View]
I want to make some brief comments and point out a few minor errors.
> A lot of web sites already provide such
> functionality. Unfortunately, these websites
> often have the filename extensions .asp or
> .aspx, and not .php. What a shame. Who says the
> ASP folks down the street should have all the
> fun? In this article, I'll show you how you can
> add such functionality to your PHP forms.
> (Besides, have you ever tried programming in
> ASP? Frankly, PHP is a lot easier!)"
Validating form data is a fundamental activity of Web development, thus any language will suffice. The key is to never rely on client-side validation, as this is as insecure and unrealiable as it sounds.
> Or, if you're sneaky, you might manipulate the
> headers and send the user back to the original
> processlogin.php page.
You mean back to the original login.php page, right?
> There's another way to do it that's a little
> cleaner, and, more importantly, allows you to
> keep your validation code right inside of the
> login.php page.
You're trying to argue that your approach is cleaner, but you don't offer any justification or defense of this statement. I believe that the cleanliness of the approach has almost nothing to do with the URL that the POST request is sent to and almost everything to do with the design of the application that handles and responds to that request.
> header("location: loginsucceeded.php");
The format of the Location header is that it begins with an uppercase L and consists of a single absolute URL. See section 14.30 of RFC 2616 for verification.
As PHP developers, we should adhere to the standards of the Web whenever possible, and using relative URLs in Location headers has also been known to cause problems with certain browsers (cookies not being included in the GET request for the new URL).
> $urlname = urlencode($$_POST['username']);
If $_POST['username'] is chris, then this will attempt to URL encode the value of $chris. Was the second $ intended?
If not, there is also the problem that the form doesn't include an element named username. Perhaps this was meant to be as follows:
$urlname = urlencode($_POST['name']);
In an article about data validation, it might be good to also point out that the thankyou.php script should treat $_GET['username'] as tainted data (this would be true even if proper validation was performed on $_POST['name'] in this example, which would have been a good idea as well).
> thankyou.php?&username=$urlname
I believe the & is unintentional here.
Anyway, I hope these comments are helpful.


