# Registration form with validation and error messages

I've been working on a registration form page in jQuery Mobile and I think I'm starting to get it fully complete.

What I need feedback on is if I've forgotten anything in terms of accessibility, security (am I open to any SQL injections or other risks?) and just anything in general. It's my first time creating a proper registration form like this so surely there must be things to improve. The labels are in Swedish but you can see the translation in the $validation echo. I would also like some feedback on my PHP validation because it doesn't feel very dry at all with all the if statements. Perhaps you can combine them in some clever way? Form markup:  <form id="register-form" method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">
<div id="register-inputs-wrapper">
<label for="fname" class="bold">Förnamn:<span class="warning-text"><?php
if(isset($validation['firstname'])) { echo$validation['firstname'];
}
?></span>
</label>
<input type="text" name="fname" id="fname" value="<?php echo $firstname; ?>" placeholder="Förnamn" alt="Skriv ditt förnamn" data-clear-btn="true"> <label for="lname" class="bold">Efternamn:<span class="warning-text"><?php if(isset($validation['lastname'])) {
echo $validation['lastname']; }?></span> </label> <input type="text" name="lname" id="lname" value="<?php echo$lastname; ?>"
placeholder="Efternamn" alt="Skriv ditt efternamn" data-clear-btn="true">

if(isset($validation['address'])) { echo$validation['address'];
}?></span>
</label>
value="<?php echo $address; ?>" placeholder="T.ex. Kungsgatan 40" alt="Skriv din gatuaddress" data-clear-btn="true"> <label for="postal-code" class="bold">Postnummer:<span class="warning-text"><?php if(isset($validation['postal-code'])) {
echo $validation['postal-code']; }?></span> </label> <input type="text" name="postal-code" id="postal-code" value="<?php echo$postal_code; ?>" placeholder="T.ex. 333 33"
alt="Skriv ditt postnummer" data-clear-btn="true">

if(isset($validation['city'])) { echo$validation['city'];
}?></span>
</label>
<input type="text" name="city" id="city" value="<?php echo $city; ?>" placeholder="Stad/ort" alt="Skriv din stad eller ort" data-clear-btn="true"> <label for="education" class="bold">Utbildning:<span class="warning-text"><?php if(isset($validation['education'])) {
echo $validation['education']; }?></span> </label> <select name="education" id="education" alt="Välj utbildning i listan" data-wrapper-class="ui-btn ui-btn-inline"> <option value="Cobolutvecklare">Cobolutvecklare</option> <option value="Programvarutestare">Programvarutestare</option> <option value="Projektledare">Projektledare</option> <option value="Webbutvecklare">Webbutvecklare</option> <option value="Webbutvecklare">Ingen (admin, lärare)</option> </select> <label for="user-code" class="bold">Behörighetskod:<span class="warning-text"><?php if(isset($validation['usertype'])) {
echo $validation['usertype']; }?></span> </label> <input type="password" name="user-code" id="user-code" value="<?php echo$usertype; ?>" placeholder="4 siffror (XXXX)"
alt="Skriv din behörighetskod" data-clear-btn="true">

<label for="email" class="bold">Email:<span class="warning-text"><?php
if(isset($validation['email'])) { echo$validation['email'];
}
if (isset($validation['existing-user'])) { echo$validation['existing-user'];
}
?></span>
</label>
<input type="email" name="email" id="email" value="<?php echo $email; ?>" placeholder="Exempel@jensen.se" alt="Skriv din e-post address" data-clear-btn="true"> <label for="pw" class="bold">Lösenord:<span class="warning-text"><?php if(isset($validation['password'])) {
echo $validation['password']; } ?></span> </label> <input type="password" name="password" id="pw" value="<?php echo$password; ?>"
placeholder="T.ex. Jensenonline038"
alt="Skriv ett lösenord" data-clear-btn="true">

<label for="conf-pw" class="bold ui-btn-inline">Bekräfta lösenord:<span class="warning-text left"><?php
if(isset($validation['conf_password'])) { echo$validation['conf_password'];
}
?></span>
</label>
value="<?php echo $confirmed_password; ?>" placeholder="Bekräfta lösenordet" alt="Bekräfta lösenordet" data-clear-btn="true"> </div> </form> <div class="center-text"> <input type="submit" form="register-form" name="submit_reg" value="Registrera" alt="Slutför registrering" data-icon="check" data-iconpos="right" data-inline="true" data-wrapper-class="space-upper" id="reg-submit"> <div> <a href="#landing-page" class="ui-btn ui-btn-inline ui-corner-all ui-shadow ui-btn-icon-left ui-icon-arrow-l space-top">Gå tillbaks</a> </div> </div>  PHP validation code: function validate_registration($firstname, $lastname,$address, $postal_code,$city, $usertype,$email, $password,$confirmed_password) {

//Tell the server that we're accessing the global $db variable global$db;

//Clear the previous errors to ensure
//that the correct error messages is being displayed
$fnameErr =$lnameErr = $addressErr =$postal_codeErr = $cityErr =$usertypeErr = $emailErr =$pwErr = $conf_pwErr =$ex_emailErr = '';

//Remove any excess whitespace
$firstname = trim($firstname);
$lastname = trim($lastname);
$address = trim($address);
$postal_code = trim($postal_code);
$city = trim($city);
$usertype = trim($usertype);
$email = trim($email);
$password = trim($password);
$confirmed_password = trim($confirmed_password);

//Check that the input values are of the proper format
if (!preg_match('/^[A-Za-zéåäöÅÄÖ\s\ ]*$/',$firstname)) {
$fnameErr = 'Förnamnet kan endast innehålla bokstäver (é, a-ö) och mellanslag'; } if (!preg_match('/^[A-Za-zéåäöÅÄÖ\s\ ]*$/', $lastname)) {$lnameErr = 'Efternamnet kan endast innehålla bokstäver (é, a-ö) och mellanslag';
}
if (!preg_match('/^[A-Za-z0-9éåäöÅÄÖ\s\ ]*$/',$address)) {
$addressErr = 'Addressen kan endast innehålla bokstäver (é, a-ö), siffror och mellanslag'; } if (!preg_match('/^(se-|SE-){0,1}[0-9]{3}\s?(| |-)[0-9]{2}$/', $postal_code)) {$postal_codeErr = 'Ogiltigt format)';
}
if (!preg_match('/^[A-Za-zéåäöÅÄÖ\s\ ]*$/',$city)) {
$cityErr = 'Endast bokstäver är tillåtna (é, a-ö)'; } if (!preg_match('/^\S*(?=\S{8,})(?=\S*[a-z])(?=\S*[A-Z])(?=\S*[\d])\S*$/', $password)) {$pwErr = 'Minst 8 tecken, en versal och en siffra';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {$emailErr = 'Ogiltig e-post address';
}
if (empty($firstname)) {$fnameErr = 'Du måste ange ditt förnamn';
}
if (empty($lastname)) {$lnameErr = 'Du måste ange ditt efternamn';
}
if (empty($address)) {$addressErr = 'Du måste ange en address';
}
if (empty($city)) {$cityErr = 'Du måste ange din stad eller ort';
}
if (empty($email)) {$emailErr = 'Du måste ange en e-post address';
}
if (empty($password)) {$pwErr = 'Du måste ange ett lösenord';
}
if (empty($confirmed_password)) {$conf_pwErr = 'Du måste bekräfta lösenordet';
}
if ($confirmed_password !=$password && !empty($confirmed_password)) {$conf_pwErr = 'Lösenorden matchar inte';
}
try {
$query = 'SELECT * FROM usertypes';$prepared_stmt = $db->prepare($query);
$prepared_stmt->execute();$valid_codes = $prepared_stmt->fetch(PDO::FETCH_ASSOC); if ($valid_codes['admin'] === $usertype) {$usertype = 'Admin';
}
else if ($valid_codes['teacher'] ===$usertype) {
$usertype = 'Teacher'; } else if ($valid_codes['student'] === $usertype) {$usertype = 'Student';
}
else {
$usertypeErr = 'Ogiltig behörighetskod'; } if (empty($usertype)) {
$usertypeErr = 'Du måste ange en behörighetskod'; } else if (!ctype_digit($usertype)) {
$usertypeErr = 'Endast siffror tillåtet'; } else if (strlen($usertype) > 4) {
$usertypeErr = 'Koden är för lång'; } else if (strlen($usertype) < 4) {
$usertypeErr = 'Koden är för kort'; } } catch (Exception$e) {
echo $e; } try {$query = 'SELECT * FROM useraccounts ';
$query .= 'WHERE email = :email';$prepared_stmt = $db->prepare($query);
$prepared_stmt->bindParam(':email',$email);
$prepared_stmt->execute();$user_exists = $prepared_stmt->fetch(); if($user_exists){
$ex_emailErr = 'Den angivna e-post addressen existerar redan, vänligen ange en annan giltig e-post address'; } } catch (Exception$e){
echo $e; } if (empty($fnameErr) &&
empty($lnameErr) && empty($addressErr) &&
empty($postal_codeErr) && empty($cityErr) &&
empty($usertypeErr) && empty($emailErr) &&
empty($pwErr) && empty($conf_pwErr)) {
return array(
'state' => true,
'usertype_text' => $usertype ); } else { return array( 'firstname' =>$fnameErr,
'lastname' => $lnameErr, 'address' =>$addressErr,
'postal-code' => $postal_codeErr, 'city' =>$cityErr,
'usertype' => $usertypeErr, 'email' =>$emailErr,
'password' => $pwErr, 'conf_password' =>$conf_pwErr,
'existing-user' => $ex_emailErr ); } }  • From a UX perspective, I suggest you look into implementing Google Places API. It can help reduce user errors, and help with validation as well. – Alex L Jan 10 '15 at 19:50 • @AlexL How does a geographic API help me when validating user information?.. – Chrillewoodz Jan 10 '15 at 20:21 • The Place Search will help users select a location. This way they can search for an address, which Google will help them find accurately, and then in return you can get from that selection a location based on longitude and latitude. Now look at that, you don't have to worry about pesky, million-ways-to-format addresses! – Alex L Jan 10 '15 at 20:25 • @AlexL That's not an issue in this case since all the users will be living in Sweden. But I will keep it in mind for future projects involving addresses, cheers. – Chrillewoodz Jan 11 '15 at 9:02 • You might be interested in this meta. Honestly, we could use someone who knows what they're talking about to weigh in. – RubberDuck Jan 15 '15 at 13:17 ## 2 Answers # Security First the good news: You use prepared queries which is a good thing as it prevents SQLInjection one of the most nasty and common security breaches. Also you escape output to prevent XSS. But you should improve the following things: Add a CSRF token!!, otherwise a new administrator can be added by performing a CSRF attack on one of your users that are allowed to add users. As a general rule add tokens to all forms that use method="post" and for all actions that need a user to be logged in (like log-out links). What do you do with the input variables before you pass them into validation_registration? I recommend extracting your input variables from $_GET and $_POST in the same place where you validate them, so that you never pass around unnecessary dangerous input. Do not use $_SERVER['PHP_SELF'] it's an unnecessary dangerous input variable which can be easily modified by an attacker. Instead define a constant base path in your config file like this:

define('BASE_URL', '//example.com');


Then you can write <?php echo BASE_URL; ?>/subdir in your template files.

Always escape all variables in your template files that are not supposed to contain html. Even if you have filtered them. So never write something like: echo $validation['email'];. Always use htmlspecialchars with ENT_QUOTES because else ' will not be escaped: very dangerous! For easy use it's best to define a very short global function that will escape your values properly, like this: /** * Escape given input for the use in HTML. * * @param String$input
*  Unescaped input.
*
* @return String
*  Escaped input.
*/
function e($input) { // Use htmlspecialchars with ENT_QUOTES to escape '. return echo htmlspecialchars($variable, ENT_QUOTES, 'UTF-8');
}


# Structure

Your current validation functions is a bit hard to read. You can tidy it up a lot if you outsource some things to own functions, use a loop and filter_var like this:

<?php
/**
* Check if a user with given e-mail exists.
*
* @param String $email * The e-mail to id the user. * * @return Boolean * True -> User exists. * False -> User does not exists. */ function user_exits($email)
{
global $db;$query = 'SELECT count(*) as c FROM useraccounts';
$query .= 'WHERE email = :email';$prepared_stmt = $db->prepare($query);
$prepared_stmt->bindParam(':email',$email);
$prepared_stmt->execute();$row = $prepared_stmt->fetch(PDO::FETCH_ASSOC); if (empty($row) || ($row['c'] < 1)) { return false; } else { return true; } } /** * Get a list of possible user types. * * Matches usertype id to readable user type. */ function get_user_types() { global$db;

// Get one row from usertypes table and extract field names.
$query = 'SELECT * FROM usertypes';$prepared_stmt = $db->prepare($query);
$prepared_stmt->execute();$row = $prepared_stmt->fetch(PDO::FETCH_ASSOC); // Swap keys and values. return array_combine($row, array_keys($row)); } /** * Validate user input from registration form. * * @warning * This function processes unfiltered user input! * * @param String$firstname
*  The users first name.
*
* @param String $lastname * The users last name. * * @param String$address
*  Street and house number of the users residence.
*
* @param String $postal_code * Swedish zip code of the users residence. * * @param String$city
*  The city of the users residence.
*
* @param String $usertype * The type of user, for example teacher. * * @param String$email
*  The email of the user.
*
* @param String $password * The users password in clear text. * * @param String$confirmed_password
*  The repeated input of the users password in clear text.
*
* @return Array
*  On error an Array with errormessages.
*  On success an Array containing a success flag and the usertype as text.
*/
function validate_registration(
$firstname,$lastname,
$address,$postal_code,
$city,$usertype,
$email,$password,
$confirmed_password ) { // Regexp to match text against wanted characters.$text_regxp = '/^[A-Za-zéåäöÅÄÖ\s\ ]*$/';$usertypes = get_user_types();

// Callbacks for filters.
$passwords_match = function() use ($password, $confirmed_password) { return ($password == $confirmed_password); };$usertype_exists = function() use ($usertypes) { return in_array($usertype, array_keys($usertypes)); };$user_does_not_exists = function () use($email) { return !user_exists($email);
};

$filters = array( array( 'field' => 'firstname', 'var' =>$firstname,
'filter' => FILTER_VALIDATE_REGEXP,
'filter_options' => array('regexp' => $text_regxp), 'error_msg' => 'Förnamnet kan endast innehålla bokstäver (é, a-ö) och mellanslag', 'required' => true, 'empty_msg' => 'Du måste ange ditt förnamn', ), array( 'field' => 'lastname', 'var' =>$lastname,
'filter' => FILTER_VALIDATE_REGEXP,
'filter_options' => array('regexp' => $text_regxp), 'error_msg' => 'Efternamnet kan endast innehålla bokstäver (é, a-ö) och mellanslag', 'required' => true, 'empty_msg' => 'Du måste ange ditt efternamn', ), array( 'field' => 'address', 'var' =>$address,
'filter' => FILTER_VALIDATE_REGEXP,
'filter_options' => array('regexp' => $text_regxp), 'error_msg' => 'Efternamnet kan endast innehålla bokstäver (é, a-ö) och mellanslag', 'required' => true, 'empty_msg' => 'Du måste ange en address', ), array( 'field' => 'city', 'var' =>$city,
'filter' => FILTER_VALIDATE_REGEXP,
'filter_options' => array('regexp' => $text_regxp), 'error_msg' => 'Endast bokstäver är tillåtna (é, a-ö)', 'required' => true, 'empty_msg' => 'Du måste ange din stad eller ort' ), array( 'field' => 'postal_code', 'var' =>$postal_code,
'filter' => FILTER_VALIDATE_REGEXP,
'filter_options' => array('regexp' =>
'/^(se-|SE-){0,1}[0-9]{3}\s?(| |-)[0-9]{2}$/'), 'error_msg' => 'Ogiltigt format', 'required' => true, 'empty_msg' => 'You must enter a postal code', ), array( 'field' => 'email', 'var' =>$email,
'filter' => FILTER_VALIDATE_EMAIL,
'filter_options' => null,
'required' => true,
'empty_msg' => 'Du måste ange en e-post address',
),
array(
'field' => 'email',
'var' => $email, 'filter' => FILTER_VALIDATE_CALLBACK, 'filter_options' =>$user_does_not_exists,
'error_msg' => 'An user with that e-mail already exists.',
),
array(
'var' => $password, 'filter' => FILTER_VALIDATE_REGEXP, 'filter_options' => array('regexp' => '/^\S*(?=\S{8,})(?=\S*[a-z])(?=\S*[A-Z])(?=\S*[\d])\S*$/'),
'error_msg' => 'Minst 8 tecken, en versal och en siffra',
'required' => true,
'empty_msg' => 'Du måste ange ett lösenord',
),
array(
'var' => $confirmed_password, 'filter' => FILTER_CALLBACK, 'filter_options' =>$passwords_match,
'required' => true,
'empty_msg' => 'Du måste ange en e-post address',
),
array(
'field' => 'usertype',
'var' => $usertype, 'filter' => FILTER_CALLBACK, 'filter_options' =>$usertype_exists,
'error_msg' => 'Du måste ange en behörighetskod',
)
);

// Stores error messages for fields.
$error_messages = array(); // Filter user input. foreach ($filters as $filter) { // If that field is required: check that it is not empty. // If it is empty record an error message and continue with the next filter. if ($filter['required'] && empty($filter['var'])) {$error_messages[$filter['field']] =$filter['empty_msg'];

continue;
}

// Apply the filter using filter_var and save the result in status.
$status = filter_var($filter['var'], $filter['filter'], array( 'options' =>$filter['filter_options']
));

// If the filter result is false: record an error message.
if (!$status) {$error_messages[$filter['field']] =$filter['error_msg'];
}
}

if(empty($error_messages)) { return array( 'state' => true, 'usertype_text' =>$usertypes[$usertype], ); } else { return$error_messages;
}
}


### DB-Errors

Your current code catches DB-Errors in your validation function. While you are correct that it is important to handle errors, this is not the right place to do it. A DB-Error will make your application useless therefore you want it to reach the main try catch block of your application, in which you should handle the error.

### OOP

You use a global variable for storing your DB connection, better use a singleton object. As a general rule avoid globals. Using a singleton with lazy initializing will reduce load time on pages that do not interact with the DB and will create cleaner code.

Use MVC to separate your output and input logic. Send your form to the controller and let it process the users input. Your view object will get the data to display from the model and then include the template file which accesses the variables from the view.

Use a user object for your user which you initialize with an array and that you can revert back to an array to store it in the DB.

Pass objects or arrays instead of extreme long parameter lists, like you did in validate_registration.

### Tools

Writing the server backend from scratch might be good for learning, but in production it makes sense to use a framework like Laravel, Zend Framework, PHP Cake, CodeIgniter, .... They include basic functionality like routing, MVC, form validation, User registration, etc. Or even a CMS like Drupal, Wordpress, ....

# Style

### Documentation

Document your functions and classes! Use a pattern for documentation like Doxygen (The pattern I used in the posted code above). Be sure to document the type of your input and return variables, because PHP is a typeless language.

Break long lines into multiple lines! Use a limit of 80-120 characters per line.

• Under "structure" you said "use a loop and filter_var like this:" but I didn't see either of them being used in the example. Care to explain what you meant there? Also, how do I pass the errors to the main try catch block in a smart way? – Chrillewoodz Jan 19 '15 at 13:59
• I loop through the array $filters using foreach and then call filter_var with the params from $filter in the the loop. Create a try catch block around your main program and just don't catch DB Exceptions in your filter function, that way they travel up the call stack until they are caught by your main try and catch block. – Gellweiler Jan 19 '15 at 14:38
• I've cleaned up the loop, hopefully that makes it easier for you to understand what I'm doing. It is OK to learn function oriented programming fist before diving into OOP, so you can ignore the OOP and Tools section for now and return to them when you're ready to take that step. – Gellweiler Jan 19 '15 at 14:49

Usability: Names

You might want to check out Falsehoods Programmers Believe About Names. Depending on the country your website will target, relying on first + last name might be fine, but I would definitely not filter the name.

For example, you allow öäü, which lets me to believe that you accept users with german names. But still, these people could not sign up.

I don't know that much about alphabets in other languages, but looking at this wiki page it seems that you are missing quite a lot of punctuation, not to mention non-latin alphabets.

Just let users enter their unfiltered names and rely on the standard protections against XSS and SQL injection. And as Gellweiler mentioned, use ENT_QUOTES, so that people named John O' onMouseOver='alert("test"); do not cause problems.

Security

When echoing variables that could be user supplied, I would always use htmlspecialchars right where you are echoing it (eg echo htmlspecialchars($variable, ENT_QUOTES, 'UTF-8');. Do not rely on filtering that might be done at other places. Misc • comments: your current comments do not really add anything to the code (eg: Tell the server that we're accessing the global$db variable: that should be obvious to anyone who knows PHP). I would like to see documentation on functions (eg return values, and that validate_registration actually performs database access), and possibly the regexes (because complex regexes are often hard to grasp on first look)
• functions: you can make your code more readable and reusable if you split it up into functions containing logical blocks of code. For example: validateUserInput, getUserTypes, validateUserType, and userExists.
• it doesn't seem to be a good idea to trim the password. What if my password is password? Your code would not have a problem with that, but now I actually don't know my correct passwords. I actually would not trim any of the values, as it is very unlikely that a user would add a space on accident. But definitely don't trim password fields.
• mixing of camelCase and under_score is a bit confusing. I would choose one and stick with it.
• I would try to choose one identifier for one thing and then stick with it (eg conf-pw vs conf_password vs confirmed_password) to make it easier to recognize and remember.
• also note that sometimes you switch how different identifiers in the same context look (eg $validation['conf_password'] (underscore) vs $validation['existing-user'] (dash) vs \$validation['usertype'] (nothing)), which will make it really difficult to remember these.
• With naming, isn't it still a good idea to not allow numbers? Because I don't know if there is any country in the world where numbers actually appear in the name.. The only way it could happen I guess would be if someone is called "the 3rd" but that can easily be written as "the third". Good idea or not? – Chrillewoodz Jan 19 '15 at 14:02
• @Chrillewoodz well, why would they want to write the third if their actual name is the 3rd? There are always ways around any restrictions (ß -> ss, ö -> oe, just make up a completely new name, ...), but I don't really see any benefit in limiting what a person can be named. – tim Jan 19 '15 at 14:12