Improving the Simple Contact Form
Adding Better Error Checking and Basic Spam Protection
A few weeks back, we learned how to build a simple contact form to allow our users to send us feedback directly from a site.
As noted in the comments, there were a few shortcomings with the contact form. Most notably, there was no spam protection whatsoever. Additionally, if the user made a mistake, their message would be lost, which is extremely inconvenient.
To make this contact form ready to use on a real website, we'll be adding both a basic spam protection system and the ability to store the message in a session in case the user makes a mistake.
NOTE: Like all projects built on this blog, this is being presented as a teaching exercise and a demonstration, so be sure to double-check it before using it in a project.
View the Demo | Download the Source
Understanding the Problem, Part 1: Better Error Handling
One of the most annoying things our web form could do is allow us to spend twenty minutes crafting a message, only to realize that we forgot to add the ".com" to our email address and therefore had our information rejected by the application and lost. (By the way, I know the comment form on this blog does that. It's on my to-do list, I swear!)
To save our users a lot of frustration, we'll need to update our contact form to save the information entered before checking for validity. This allows us to return an error message to the user telling them what went wrong, as well as keeping their original message intact.
Understanding the Problem, Part 2: Spam Protection
Because there's still that special kind of asshole that builds robots to find and spam any available web form, we need to guard our form against automated submissions.
The most popular method for deterring robots is to use CAPTCHA, or a type of challenge-response test that essentially proves that a user is human. This form of spam protection is really effective, but can also have the unwanted side-effect of protecting your form against people as well, since the warped letters can be hard to read.
All that a CAPTCHA does, though, is gives the user an instruction (such as, "Type the letters from the image below") and checks their response. Because our form is not granting access to sensitive data, we don't need to generate a bulletproof spam protection service. Therefore, we're going to use a much simpler approach: we're going to ask a basic arithmetic question and check the answer.
Because it takes a relatively smart spambot to interpret a question, we're going to trust that only a human will understand the question, "What is 4 + 5?" If the user gives us the answer, 9, then we know with relative certainty that the message is legitimate.
Solving the Problem
For this project, I'm going to assume that everyone's familiar with the original post. All code will be presented, but the explanation for how the code works will be omitted. Refer to the original post for explanations on the parts of the contact form not covered here.
This project consists of four files:
- index.php — contains our form and error handling
- contact.inc.php — processes submitted messages and returns a status message
- default.css — CSS styles for the form
- beautiful-forms.js — adds JavaScript form validation and cleans up forms (read about it here)
Step 1 — Adding Better Error Handling
Originally, our error handling only involved outputting a basic message and killing the execution of our script. Obviously, this isn't acceptable for a real site, so we're going to improve it.
Saving the Submitted Information in Case of Errors
First and foremost, we want to remove the annoyance of needing to re-enter information in the event of a mistake on the form. To do this, we're going to use session variables.
In order to use sessions, we need to have a session available to us, which we accomplish using session_start() at the very top of any file that will need access to the saved information. In this case, we'll need to add the call to both index.php and contact.inc.php for our script to work.
<?php
// Start the session
session_start();
After we open the session, we remove the whitespace from before and after the form's submitted values using a really handy function called array_map(), which allows us to call a function on each element of an array. We're using it here to call the trim() function on all submitted values in the $_POST superglobal. The return value of array_map() will then be stored in a variable, which we'll simply call $p.
<?php
// Start the session
session_start();
/*
* If the form was submitted and the "Send" button pressed,
* continue processing the data
*/
if($_SERVER['REQUEST_METHOD']=='POST' && $_POST['submit']=='Send')
{
// Trim all the values to remove unwanted whitespace
$p = array_map('trim', $_POST);
With our values trimmed and stored, we're ready to save them in the session. To do this, we simply create an array element in the $_SESSION superglobal for each form field and set its value to the submitted information.
<?php
// Start the session
session_start();
/*
* If the form was submitted and the "Send" button pressed,
* continue processing the data
*/
if($_SERVER['REQUEST_METHOD']=='POST' && $_POST['submit']=='Send')
{
// Trim all the values to remove unwanted whitespace
$p = array_map('trim', $_POST);
// Store the posted information as session variables
$_SESSION['cf_n'] = $p['cf_n'];
$_SESSION['cf_e'] = $p['cf_e'];
$_SESSION['cf_w'] = $p['cf_w'];
$_SESSION['cf_h'] = $p['cf_h'];
$_SESSION['cf_m'] = $p['cf_m'];
Now the submitted information is available to our script, even if the form submission fails, allowing us to grant our users another chance to get the form right without causing them much grief. We'll get to how this information is used in just a moment.
Using Status Codes to Identify Errors
Next we need to modify our error handling. In the original post, we simply saved an error message in a variable, which was output to the browser using echo() in the event of an error. Our new approach is to catch an error by sending the user back to the form with a status code that identifies it.
For simplicity's sake, we're going to use the following status codes:
- 0 — No error
- 1 — Anti-spam question incorrect or incomplete
- 2 — No name submitted
- 3 — Invalid email submitted
- 4 — No message submitted
- 5 — Sending of message failed
- 6 — Sending of confirmation message failed
Each step in the validation process checks for an individual field's validity. If the field does not meet our criteria for being considered "valid", we send our status code for the error as a URL query string and use header() to direct the user back to the form, where an error message will be displayed.
Modify contact.inc.php to contain the following code:
<?php
// Start the session
session_start();
/*
* If the form was submitted and the "Send" button pressed,
* continue processing the data
*/
if($_SERVER['REQUEST_METHOD']=='POST' && $_POST['submit']=='Send')
{
// Trim all the values to remove unwanted whitespace
$p = array_map('trim', $_POST);
// Store the posted information as session variables
$_SESSION['cf_n'] = $p['cf_n'];
$_SESSION['cf_e'] = $p['cf_e'];
$_SESSION['cf_w'] = $p['cf_w'];
$_SESSION['cf_h'] = $p['cf_h'];
$_SESSION['cf_m'] = $p['cf_m'];
// If the name field was filled out, sanitize the input and store it
if(!empty($p['cf_n']) && $p['cf_n']!='Name (required)')
{
$name = htmlentities(stripslashes($p['cf_n']), ENT_QUOTES);
}
else
{
// If the name wasn't entered, create an error message
header("Location: ../?status=2");
exit;
}
// If the email is set, validate and store it
if(!empty($p['cf_e']) && $p['cf_e']!='Email (required)')
{
// Define a regex pattern to validate the email address
$pattern = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$/i";
if (preg_match($pattern, $p['cf_e']))
{
$email = $p['cf_e'];
}
else
{
// If the email doesn't match the pattern, error
header("Location: ../?status=3");
exit;
}
}
// If no email was entered, generate an error message
else
{
header("Location: ../?status=3");
exit;
}
// Check the website field
if(!empty($p['cf_w']) && $p['cf_w']!='Website (optional)')
{
$site = htmlentities(stripslashes($p['cf_w']), ENT_QUOTES);
}
else
{
// Because the website is optional, no error if empty
$site = "none";
}
// Ensure a message was entered, then sanitize it
if(!empty($p['cf_m']) && $p['cf_m']!='Enter Your Message Here')
{
$message = strip_tags(stripslashes($p['cf_m']));
}
// If no message was entered, adds an error message
else
{
header("Location: ../?status=4");
exit;
}
/*
* If no errors occurred, send the message
*/
$to = "Ennui Design <answers@ennuidesign.com>";
$subject = "[Ennui Design] Message from the Ennui Design contact form";
$headers = <<<MESSAGE_HEADER
From: $name <$email>
Content-Type: text/plain
MESSAGE_HEADER;
$msg = <<<MESSAGE_BODY
Name: $name
Email: $email
URL: $site
Message:
$message
--
This message was sent via the contact form on EnnuiDesign.com
MESSAGE_BODY;
if(!mail($to, $subject, $msg, $headers))
{
header("Location: ../?status=5");
exit;
}
// Now send a confirmation email to the user.
$conf_to = "$name <$email>";
$conf_sub = "Thank You for Contacting Us!";
$conf_headers = <<<MESSAGE_HEADER
From: Ennui Design <donotreply@ennuidesign.com>
Content-Type: text/plain
MESSAGE_HEADER;
$conf_message = <<<MESSAGE_BODY
Thank you for contacting us! Your message
was sent successfully, and we will get back
to you as quickly as possible.
All the Best,
Ennui Design
answers@ennuidesign.com
www.EnnuiDesign.com
MESSAGE_BODY;
if(!mail($conf_to, $conf_sub, $conf_message, $conf_headers))
{
header("Location: ../?status=6");
exit;
}
// Destroy the session variables
unset($_SESSION['cf_n'], $_SESSION['cf_e'], $_SESSION['cf_w'], $_SESSION['cf_h'], $_SESSION['cf_m']);
// Send the user back to the main page
header('Location: ../?status=0');
exit;
}
else
{
/*
* If the wrong request method was used or the "Send" button
* wasn't pressed (i.e. "Cancel" was pressed), return the user
* to the contact form with no message.
*/
header("Location: ../");
exit;
}
?>
Assigning Messages to Status Codes
In index.php, we're going to check if a status code was sent, then use a switch to assign a message to each status code we'll be creating.
Place the following code into index.php:
<?php
// If an error exists, generate a message to correspond
if(isset($_GET['status']))
{
$class = NULL;
switch($_GET['status'])
{
case 0:
$class = ' class="success"';
$error = "Your message was sent successfully!";
break;
case 1:
$error = "Make sure you answer the anti-spam question before sending!";
break;
case 2:
$error = "Please enter your name before sending!";
break;
case 3:
$error = "Please enter a valid email address!";
break;
case 4:
$error = "Please enter a message before sending!";
break;
case 5:
$error = "Something went wrong while sending your message. Please try again!";
break;
case 6:
$error = "Your message was sent, but the confirmation message failed. Sorry about that!";
break;
default:
$error = "Something unexpected happened. Please try again!";
break;
}
$err_msg = "<h3$class>$error</h3>";
}
else
{
$err_msg = NULL;
}
?>
Note that in the event of status code 0, we add a class named "success" to the <h3> tag. This is so that our success message doesn't get styled to look like an error.
Using Saved Data to Repopulate Form Fields
If our form encountered an error, we want to place the previously submitted information back into the appropriate fields, as well as display the error message we just stored in the variable $err_msg.
To accomplish this, we need to access the session variables we created in contact.inc.php. At the very top of index.php, before our status code check, we're going to access the session with session_start(), then check if our field variables are set. If so, we'll strip any slashes using stripslashes() and store the value as an easy-to-access variable (i.e. $name for the $_SESSION['cf_n'] variable, $email for the $_SESSION['cf_e'] variable, etc.) for later use.
<?php
session_start();
// Load the saved session info if it exists
$name = (isset($_SESSION['cf_n'])) ? stripslashes($_SESSION['cf_n']) : NULL;
$email = (isset($_SESSION['cf_e'])) ? stripslashes($_SESSION['cf_e']) : NULL;
$web = (isset($_SESSION['cf_w'])) ? stripslashes($_SESSION['cf_w']) : NULL;
$human = (isset($_SESSION['cf_h'])) ? stripslashes($_SESSION['cf_h']) : NULL;
$msg = (isset($_SESSION['cf_m'])) ? stripslashes($_SESSION['cf_m']) : NULL;
// If an error exists, generate a message to correspond
if(isset($_GET['status']))
{
To display the saved information if it exists, we need to add value attributes to our form fields. In the HTML form within index.php, modify the code to contain the following:
<h1> Simple Contact Form </h1>
<?php echo $err_msg ?>
<form action="inc/contact.inc.php" method="post" id="cf">
<fieldset>
<legend>Send a Message</legend>
<label for="cf_n">Name (required)</label>
<input id="cf_n" name="cf_n" type="text" maxlength="75"
value="<?php echo $name ?>" />
<label for="cf_e">Email (required)</label>
<input id="cf_e" name="cf_e" type="text" maxlength="150"
value="<?php echo $email ?>" />
<label for="cf_w">Website (optional)</label>
<input id="cf_w" name="cf_w" type="text" maxlength="150"
value="<?php echo $web ?>" />
<label for="cf_m">Enter Your Message Here</label>
<textarea id="cf_m" name="cf_m"
rows="14" cols="45"><?php echo $msg ?></textarea>
<input type="submit" name="submit" class="submit" value="Send" />
<input type="submit" name="submit" class="submit" value="Cancel" />
</fieldset>
</form>
Now, if we load the form in a browser and enter an incorrect email address or omit a field, we should be given an error message, and the form should still contain the values we entered before submitting.
Step 2 — Adding Spam Protection
Our first step when adding spam protection is to create a field that will be used to determine if a user is human (you may have noticed the variables referring to $human or $_POST['cf_h'] above).
Add the anti-spam field to our form in index.php:
<h1> Simple Contact Form </h1>
<?php echo $err_msg ?>
<form action="inc/contact.inc.php" method="post" id="cf">
<fieldset>
<legend>Send a Message</legend>
<label for="cf_n">Name (required)</label>
<input id="cf_n" name="cf_n" type="text" maxlength="75"
value="<?php echo $name ?>" />
<label for="cf_e">Email (required)</label>
<input id="cf_e" name="cf_e" type="text" maxlength="150"
value="<?php echo $email ?>" />
<label for="cf_w">Website (optional)</label>
<input id="cf_w" name="cf_w" type="text" maxlength="150"
value="<?php echo $web ?>" />
<label for="cf_h">Spam Check: What is 4 + 5?</label>
<input id="cf_h" name="cf_h" type="text" maxlength="4"
value="<?php echo $human ?>" />
<p class="hint">HINT: The answer is 9</p>
<label for="cf_m">Enter Your Message Here</label>
<textarea id="cf_m" name="cf_m" rows="14" cols="45"><?php echo $msg ?></textarea>
<input type="submit" name="submit" class="submit" value="Send" />
<input type="submit" name="submit" class="submit" value="Cancel" />
</fieldset>
</form>
Next, in contact.inc.php, we need to verify that the user did, indeed, answer "9" to the challenge question. To do so, near the top of contact.inc.php just after we save the form information, we can simply add one more check:
// Store the posted information as session variables
$_SESSION['cf_n'] = $p['cf_n'];
$_SESSION['cf_e'] = $p['cf_e'];
$_SESSION['cf_w'] = $p['cf_w'];
$_SESSION['cf_h'] = $p['cf_h'];
$_SESSION['cf_m'] = $p['cf_m'];
// Make sure the spam protection didn't fail
if($p['cf_h'] != '9' && $p['cf_h'] != 'nine')
{
header("Location: ../?status=1");
exit;
}
// If the name field was filled out, sanitize the input and store it
if(!empty($p['cf_n']) && $p['cf_n']!='Name (required)')
{
Our check will allow for both the answer "9" and the answer "nine", because we don't want to confuse anyone. If the form isn't filled out correctly, the user is redirected to the form with a status code. Otherwise, execution continues normally.
Some spam-bots don't acknowledge the header() command, so we use the exit; command just to be safe. In the event that a spam bot is using our contact form, it will either dead-end when the script exits, or it will end up back at the form with no idea that it failed.
This isn't bulletproof, but it works for a small site that just wants to avoid a ton of junk mail.
View the Demo | Download the Source
Summary
This form will allow a user to contact you directly through your site. It also provides usability enhancements to save the user time in the event they make a mistake while submitting the form. Our anti-spam measures are pretty low-impact, which makes it easy for a user to prove they are human, while keeping most of the bots out.
Do you have a better way to prevent spam? Any ideas on bulletproofing this script? Let me know in the comments!
Comments for This Entry
really nice way to improve contact form and also protect form spams nice tutorial.
Interesting form issues to improve the form design pattern.
Thanks
Thanks again for a great post Jason!
Thanks for sharing your wisdom Jason. I setup a test on my server and kept getting error messages from the mail server. I had to take out the names from the email addresses in the inc.php script. Works like a champ now. Guess it may depend on how the email server is set up.
was great thank you
Can someone expand on Matt's comment about "take out the names from the email addresses"?
I am giving this a try and am getting a "Please Enter Valid Email Address" when trying to submit a message with this. All the addresses that I have tried have been valid.
@Anthony:
Did you remove an input? If so, make sure you've removed all references to cf_n. Additionally, the JavaScript file that checks the contact form needs to remove the required field cf_n.
Let me know if that works for you.
Ok, If I don't use an input of "Website" on my index.php, what all should I remove in the form.php. I guess it would be references to cf_w then. I am confused on what I need to do with the Javascript file though.
Well I just removed all references to cf_w in the form.php and now when I click submit it seems like it goes through fine and it returns me to my homepage. But when I check my email, there is nothing there. So no errors, but no email either.
@Anthony:
Set your error reporting to E_ALL and see if you're getting a notice somewhere.
Other than that, just start debugging: echo out the values of variables at various points to make sure they're behaving as expected. More than likely something small is causing a big problem, but it's easy to catch if you start at the front and work forward until the problem appears.
Good luck!
Hey Jason! Sorry, but I'm so ignorant with php.
My php.ini file is set to E_ALL and E_NOTICE and E_WARNING and I am getting no errors when I send a message. What exactly do you mean by "Additionally, the JavaScript file that checks the contact form needs to remove the required field cf_n." When I open up the beautiful forms.js file, I do not see anything referencing the input boxes. Eh, I don't know. It seems like it works perfect, but just no email goes through.
Hey Jason! Sorry, but I'm so ignorant with php.
My php.ini file is set to E_ALL and E_NOTICE and E_WARNING and I am getting no errors when I send a message. What exactly do you mean by "Additionally, the JavaScript file that checks the contact form needs to remove the required field cf_n." When I open up the beautiful forms.js file, I do not see anything referencing the input boxes. Eh, I don't know. It seems like it works perfect, but just no email goes through.
@Anthony:
I'm not sure what's causing it. My advice would be to start as simple as possible by writing a test script that sends a test message.
mail("you@yoursite.com", "Test", "Test message");
If that works, start adding features back in until you find the one that's breaking. Good luck!
When I type Norwegian character, å, ø, æ, it shows Ã¥, ø, æ in email.
Can anyone tell me what I need to change?
Thanks in advance.
Well my previous comment did not show Norwegian character correctly either. One of character is a circle on a, another one is forward slash in o and the last one is combination of a and e.
You can find here. http://www.omniglot.com/writing/norwegian.htm
How can I show them correctly?
Hey awesome job man it is really rocking...
Thanks for the in depth explaination. I will be trying this method on my next project.
Benga creative
Hey great form,thanks for that. I just have a quick question, the email gets sent and arrives in my server email fine but the customer email doesnt go for some reason so its saying I have a case 6 error. Anyone know why this might be happening and if they can help me. I've just tested it on my site so can provide a link if needed. Hope you can help. Thanks. Riain.
@Riain
I would guess that something is happening with the $conf_to variable. If the name and email address aren't set properly, the mail() function will fail the way this is set up.
Check out the variables that are being passed before the confirmation message is sent. Something like this should work:
if(!mail($conf_to, $conf_sub, $conf_message, $conf_headers))
{
echo "To: ", $conf_to, "Subject: ", $conf_sub, "Message: ",
$conf_message, "Headers: ", $conf_headers;
// header("Location: ../?status=6");
exit;
}
Good luck!
Jason,thanks a million for the fast reply! Should I have changed the $conf_to area similar to the $to section above it. Meaning do I have to change this "$name " to something unique to my site? I havent touched the $conf_to"$name "; at all. If this is correct as it is then do I just pop in the code you just posted above this section? Thanks again.Riain
@Riain
The snippet I sent you was just for debugging. It'll show you what's being sent to the mail() function. The $conf_to variable should be showing something like:
John Doe <johndoe@email.com>
If the name OR email isn't present, the mail() function will fail.
Give that a shot and see if the "To" is coming out properly.
Hey, ok I figured out what was going on I have an apostrophy in my surname and I was putting that in. Once I omitted it the form worked. Couple of small things though, how do I fix that little problem, the thing is this is for a cottage rental in the west of Ireland so we will get a lot of O'Connells and O'Briens etc. So is there a way to fix that? Also with the optional website area, I changed it to phone as the client does a lot of his wheeling and dealing over the phone. But in the email that he receives it still says url and it says none. Obviously it says none cos it doesnt read a phone number as a web address but where do I need to change it so it reads as a number? Do I just need to copy the same details as the name area or do they have to be specifically numbers. Thanks!
Sorry to bother you again Jason, any luck with my couple little problems?
@Riain
I'm not sure what's going on with the apostrophes, because I just sent an email to myself using a name with an apostrophe and both the message and the confirmation arrived. Same for the URL field with a phone number.
So something must have changed between my script and yours. Good luck!
Sorry to keep pestering you Jason I promise I'll figure this out and you'll never hear from me again! I just re did the contact form and all I changed were my contact details and its all working even the phone number is coming through. Just tried it again though with the apostrophie and this is what the failed email says;
A message that you sent contained one or more recipient addresses that were
incorrectly constructed:
Riain D'arcy : missing or malformed local part
(expected word or "
@Riain
It's the htmlentities() call on the name that's causing the problem. You could simply remove it to deal with the issue.
Give that a shot.
Possibly the last question, what should this line of code look like
$site = htmlentities(stripslashes($p['cf_w']), ENT_QUOTES);
should it be?:
$site = stripslashes($p['cf_w']);
Yep.
Man alive..
Sorry man still status=6 and the same failed email sent
@Riain
I'm not sure what to tell you. It could be a server issue, but all that's left to do is start debugging.
Good luck!
Jason, I'm a monkey. I went back and checked to see had I made a mistake with anything you had said to me and no surprises I had made that name change to the $site element so thats why it wouldnt work. Its working now so thanks a million for your patience and all your help. Riain
Jason just one last problem. When I fill in all the fields the form doesnt give me that success page and also if I dont put a valid email address in the enter a valid email address php thing doesnt come up either. It just goes back to the home page with a status 3 indicator do you know where I've gone wrong? I'm so close to getting this working perfectly its driving me nutso. Its up at shipsharbourcottage.com/contact.php Thanks a million...again!!Riain
@Riain
You need to modify the path to include the contact.php in the address. You're currently redirecting users to your main page: http://shipsharbourcottage.com/?status=0 instead of http://shipsharbourcottage.com/contact.php?status=0
Good luck!
Jason,
Thanks so much thats working perfectly now. I can now get a good nights sleep thank God! It makes sense now but holy moley I didnt see it before!. Heh, if you're ever looking for a self catering cottage in Galway you know who to come to! I could do you a deal!!Thanks again. Riain.
Hi Jason.
I'm testing your contact form in a project. It's running fine in nearly all browsers.
Only in Safari 3.2.2 and Chrome 3.0.195.38 there's a problem with the spam check label. Any idea whats the problem here? The link to the project is:
http://www.cafepitu.com/test/#contact
regards Manfred
Hi Jason.
Think, I found the problem. Now I changed the maxlength from 4 to 26.
Depends on the sum of chars needed.
Spam Check: What is 4 + 5?
Sorry, another question.
Is it possible to redirect to an anchor? In my case to #contact ??
regards Manfred
Post a Comment
Want to show your face? Get a gravatar!