Honesty Rocks! truth rules.

Advanced User Account System From Scratch Part 1 of a series of advanced PHP tutorials

HOME      >>       Staff Room

8ennett

This is the start of a series of tutorials I am writing which will be interlinked with each other. This is the first part and without the files and tables we will be creating in here the other tutorials will not work. The later tutorials will include forum making, profile pages, multiple chatrooms, in-site messaging, alerts and many more.

 

###############################

Martyn Bennett's PHP Website Tutorial: Part 1

###############################

 

Requirements: PHP5, MySQL

 

Ok, in this tutorial I will be explaining from the very beginning how to create an advanced user account system with the following features:

 

Login:

 

Standard page login with username, password and image verification (to prevent brute force attacks)

Account disable due to too many incorrect login attempts

Display amount of registered users and how many are currently online

Lost password option, for resetting a users password via email

 

Register:

 

Account validation via email

Change email if not received validation email

Suggested alternatives if username is already in use

Checking if email is valid

Refilling registration form after submit if info is not valid

Verifying age is above preset amount (toggle on/off in admin)

Restricting login name to letters and numbers only and starting only with a letter

Referral system with possible rewards for referring new members

 

 

================================================================

 

1A: Creating the MySQL tables

 

Ok this is fairly straight forward and self explanatory. You will need to create a new database and name it whatever you wish. Next run the following MySQL queries to create your tables.

 

CREATE TABLE IF NOT EXISTS `userlist` ( `id` int(255) NOT NULL auto_increment, `name` varchar(15) NOT NULL, `handle` varchar(15) NOT NULL, `pass` varchar(200) NOT NULL, `email` varchar(150) NOT NULL, `dob` int(40) NOT NULL, `fname` varchar(50), `lname` varchar(50), `gender` enum('Male', 'Female') NOT NULL, `validation` varchar(50) NOT NULL, `changepass` varchar(50) NOT NULL, `regip` varchar(15) NOT NULL, `regdate` int(40) NOT NULL, `referrer` int(255) NOT NULL, `namecolour` varchar(6) NOT NULL, `disabled` int(40) NOT NULL, `banned` enum('Yes', 'No') NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=latin1;CREATE TABLE IF NOT EXISTS `logins` (`id` int(255) NOT NULL auto_increment,`user` int(255) NOT NULL, `ip` varchar(15) NOT NULL, `time` int(40) NOT NULL, `success` enum('Yes', 'No') NOT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1;CREATE TABLE IF NOT EXISTS `online` ( `id` int(255) NOT NULL auto_increment, `user` int(255) NOT NULL,`time` int(40) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Now you may have noticed that in the userlist table there is both a name and handle field. This is for use in a later tutorial where you can edit your profile. You will be able to change your in-site handle but your login name will remain the same. Also, there is a field named 'namecolour', again this will be dealt with in the later tutorial.

 

================================================================

 

1B: Standard configuration and library files

 

We will now deal with how the main part of the site will be configured and create the necessary files. First of all we need an index.php file in the public root directory of our site. The way our site will be configured is through a system of $_GET commands instead of the usual method of linking to individual files (eg. http://ww38.yoursite.com/?l=login instead of http://ww38.yoursite.com/login.php). This will help to reduce the amount of people looking to study your file structure.

 

Place the following file in your root directory:

 

index.php

<?php $pageactivator = true; include('lib/functions.php'); opendb(); if ($_SESSION['user']['id'] > 0){ $refreshsesh = mysql_query("SELECT * FROM userlist WHERE id='".$_SESSION['user']['id']."'"); $_SESSION['user'] = mysql_fetch_array($refreshsesh); include('home.php'); } else {if ($_GET['l'] == 'register'){include('reg.php');} elseif ($_GET['l'] == 'lostpass'){ include('lp.php'); } elseif ($_GET['l'] == 'verify'){ include('ver.php');}else { include('log.php'); } } closedb(); ?>

Now this index file is going to be our hub where all our sites traffic will be directed through. The very first part of the php is $pageactivator = true; which is going to be the security blanket for the rest of our site. Only the index file will set this variable and every other page in the site will first test this variable to see if it is true. If not then it will redirect the user back to the index page. This will just add an extra layer of security to all your files and prevent manipulation should you miss something out when adding to your site later on.

 

You will also notice we are including a file named 'functions.php' in a folder named 'lib'. This is what will contain all our custom functions and classes. Custom functions are very useful if you need to perform a series of actions more than once. Creating a custom function in one place means you only have to write it the once and can use it as many times as you like. Also, because this is included in our hub then the functions will be available to every other page in your site.

 

After this there is a function name 'opendb();'. There is also one named 'closedb();' at the end of the document. These are included in the 'lib/functions.php' document and will be explained.

 

Next we test if the $_SESSION['user'] variable has been applied. This variable contains an array of all the users information from the table 'userlist' in the database. If it is not set then the user has not yet logged in (as logging in is the only way to set this array) and the script then tests if you are trying to get to the registration, lost password, account verification or login page. If it is set then it will take you to the default home page, but this is not covered in this tutorial and will be in the next tutorial.

 

Now we have our index.php file you will need to create a new subdirectory in your root folder named 'lib' (eg. http://ww38.yoursite.com/lib). Inside this subdirectory we are going to create the following file.

 

lib/config.php

<?php if ($pageactivator == true){ $confighost = 'localhost'; $configuser = 'username'; $configpass = 'password'; $configdb = 'mydatabase'; } else {header('Location: ../index.php'); } ?>

This is obviously our database config file. Replace 'localhost' with the host of your database (generally localhost anyway), 'username' with your username and etc. Notice how it opens the page by testing if the $pageactivator variable is true as well and if not redirects back to index.php in the root directory.

 

At this point i would like to point out it is a good idea to place a document in each subdirectory named index.php and have it simply contain the php command 'header('Location: ../index.php');'.

 

In the same 'lib' folder we are now going to make our functions.php document.

 

lib/functions.php

<?php if ($pageactivator == true){ // Open database connection function opendb(){ include('lib/config.php'); mysql_connect ($confighost, $configuser, $configpass); mysql_select_db ($configdb); } // Close database connection function closedb(){ include('lib/config.php');mysql_close(mysql_connect ($confighost, $configuser, $configpass)); } // Cleans up user inputted data function CleanUp($data, $sql) {$data = trim(htmlentities(strip_tags($data)));if ($sql == true){$data = mysql_real_escape_string($data);}return $data; } // Checks if an input email address is valid or not class EmailAddressValidator { /** * Check email address validity * @param strEmailAddress Email address to be checked * @return True if email is valid, false if not */ public function check_email_address($strEmailAddress) { // If magic quotes is "on", email addresses with quote marks will // fail validation because of added escape characters. Uncommenting // the next three lines will allow for this issue. //if (get_magic_quotes_gpc()) { //$strEmailAddress = stripslashes($strEmailAddress); //} // Control characters are not allowed if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $strEmailAddress)) { return false; } // Split it into sections using last instance of "@" $intAtSymbol = strrpos($strEmailAddress, '@'); if ($intAtSymbol === false) { // No "@" symbol in email. return false; } $arrEmailAddress[0] = substr($strEmailAddress, 0, $intAtSymbol); $arrEmailAddress[1] = substr($strEmailAddress, $intAtSymbol + 1); // Count the "@" symbols. Only one is allowed, except where // contained in quote marks in the local part. Quickest way to // check this is to remove anything in quotes. $arrTempAddress[0] = preg_replace('/"[^"]+"/' ,'',$arrEmailAddress[0]); $arrTempAddress[1] = $arrEmailAddress[1]; $strTempAddress = $arrTempAddress[0] . $arrTempAddress[1]; // Then check - should be no "@" symbols. if (strrpos($strTempAddress, '@') !== false) { // "@" symbol found return false; } // Check local portion if (!$this->check_local_portion($arrEmailAddress[0])) {return false; } // Check domain portion if (!$this->check_domain_portion($arrEmailAddress[1])) { return false; } // If we're still here, all checks above passed. Email is valid. return true; } /** * Checks email section before "@" symbol for validity * @param strLocalPortion Text to be checked * @return True if local portion is valid, false if not */ protected function check_local_portion($strLocalPortion) { // Local portion can only be from 1 to 64 characters, inclusive. // Please note that servers are encouraged to accept longer local // parts than 64 characters. if (!$this->check_text_length($strLocalPortion, 1, 64)) {return false; } // Local portion must be: // 1) a dot-atom (strings separated by periods) // 2) a quoted string // 3) an obsolete format string (combination of the above) $arrLocalPortion = explode('.', $strLocalPortion); for ($i = 0, $max = sizeof($arrLocalPortion); $i < $max; $i++) { if (!preg_match('.^(' .'([A-Za-z0-9!#$%&\'*+/=?^_`{|}~-]' .'[A-Za-z0-9!#$%&\'*+/=?^_`{|}~-]{0,63})' .'|' .'("[^\\\"]{0,62}")' .')$.' ,$arrLocalPortion[$i])) { return false; } } return true; } /** * Checks email section after "@" symbol for validity * @param strDomainPortion Text to be checked * @return True if domain portion is valid, false if not */ protected function check_domain_portion($strDomainPortion) { // Total domain can only be from 1 to 255 characters, inclusive if (!$this->check_text_length($strDomainPortion, 1, 255)) { return false; } // Check if domain is IP, possibly enclosed in square brackets. if (preg_match('/^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])'.'(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}$/',$strDomainPortion) || preg_match('/^\[(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])'.'(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}\]$/',$strDomainPortion)) { return true; } else {$arrDomainPortion = explode('.', $strDomainPortion); if (sizeof($arrDomainPortion) < 2) { return false; // Not enough parts to domain } for ($i = 0, $max = sizeof($arrDomainPortion); $i < $max; $i++) { // Each portion must be between 1 and 63 characters, inclusive if (!$this->check_text_length($arrDomainPortion[$i], 1, 63)) { return false; }if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|'.'([A-Za-z0-9]+))$/', $arrDomainPortion[$i])) { return false; }} } return true; } /** * Check given text length is between defined bounds * @param strTex Text to be checked * @param intMinimum Minimum acceptable length * @param intMaximum Maximum acceptable length * @return True if string is within bounds (inclusive), false if not */ protected function check_text_length($strText, $intMinimum, $intMaximum) { // Minimum and maximum are both inclusive $intTextLength = strlen($strText); if (($intTextLength < $intMinimum) || ($intTextLength > $intMaximum)) {return false; } else { return true; } } } // For determining if a year is a leap year or not function isLeapYear($year){ if ($year % 4 != 0){ return 28; } else { if ($year % 100 != 0){ return 29; } else { if ($year % 400 != 0){ return 28; } else { return 29; }} } } // Checks if server is online or offline function servStat(){$servstatfile = 'lib/ss'; $fh = fopen($servstatfile, 'r');$servstat = fread($fh, filesize($servstatfile));fclose($fh);if ($servstat == 0){ return false;}else {return true; } } // Returns a persons age from a timestamp function getAge($DOB){ $DOB = date("Y-m-d", $DOB); list($Year, $Month, $day) = explode("-", $DOB); $YearDifference = date("Y", time()) - $Year; $MonthDifference = date("m", time()) - $Month; $DayDifference = date("d", time()) - $day;if ($DayDifference < 0 || $MonthDifference < 0){ $YearDifference--; }return intval($YearDifference); } // Works out the time since (seconds) function timeSince($original) { // array of time period chunks$chunks = array(array(60 * 60 * 24 * 365 , 'year'),array(60 * 60 * 24 * 30 , 'month'),array(60 * 60 * 24 * 7, 'week'),array(60 * 60 * 24 , 'day'),array(60 * 60 , 'hour'),array(60 , 'minute'), ); $today = time(); /* Current unix time */ $since = $original - $today; // $j saves performing the count function each time around the loop for ($i = 0, $j = count($chunks); $i < $j; $i++) {$seconds = $chunks[$i][0]; $name = $chunks[$i][1];// finding the biggest chunk (if the chunk fits, break) if (($count = floor($since / $seconds)) != 0) {// DEBUG print "<!-- It's $name -->\n";break;} } $print = ($count == 1) ? '1 '.$name : "$count {$name}s"; [if ($i + 1 < $j) {// now getting the second item$seconds2 = $chunks[$i + 1][0];$name2 = $chunks[$i + 1][1];// add second item if it's greater than 0if (($count2 = floor(($since - ($seconds * $count)) / $seconds2)) != 0) {$print .= ($count2 == 1) ? ', 1 '.$name2 : ", $count2 {$name2}s";}}return $print; } // Works out the time since function timeSince2($original) {// array of time period chunks$chunks = array(array(60 * 60 * 24 * 365 , 'year'),array(60 * 60 * 24 * 30 , 'month'),array(60 * 60 * 24 * 7, 'week'),array(60 * 60 * 24 , 'day'),array(60 * 60 , 'hour'),array(60 , 'minute'), ); $today = time(); /* Current unix time */ $since = $today - $original; // $j saves performing the count function each time around the loop for ($i = 0, $j = count($chunks); $i < $j; $i++) {$seconds = $chunks[$i][0];$name = $chunks[$i][1];// finding the biggest chunk (if the chunk fits, break)if (($count = floor($since / $seconds)) != 0) {// DEBUG print "<!-- It's $name -->\n";break; }} $print = ($count == 1) ? '1 '.$name : "$count {$name}s"; if ($i + 1 < $j) {// now getting the second item $seconds2 = $chunks[$i + 1][0];$name2 = $chunks[$i + 1][1]; // add second item if it's greater than 0if (($count2 = floor(($since - ($seconds * $count)) / $seconds2)) != 0) {$print .= ($count2 == 1) ? ', 1 '.$name2 : ", $count2 {$name2}s";} } return $print; } // Returns true if all characters in text are alphanumeric function isAlphaNum($text){ if (ereg('[^A-Za-z0-9]', $text)){ return false; }else {return true; } } else {header('Location: ../index.php'); } ?>

Now this document contains all of the custom functions needed for this tutorial, just remember in future tutorials we will be adding new functions to this list. Also I can't take credit for the email validator class. This code is now public domain and freely available all over the web. The author has been lost in a sea of posers and fake credentials and no objections have been raised anywhere to free distribution and people using it.

 

================================================================

 

I will continue the rest of Part 1 tomorrow (it's late), please do not leave any comments until it is finished thank you.


8ennett

Now for the final part of our setup, we are going to create a new php file in our root directory for handling the output of our image verification:

 

randomImage.php

<?php session_start(); // make a string with all the characters that we // want to use as the verification code // 0, O, 1 and L have been removed as they can be easily mistaken in lowercase $alphanum = "abcdefghijkmnpqrstuvwxyz23456789"; // generate the verication code $rand = substr(str_shuffle($alphanum), 0, 5); // choose one of four background images $bgNum = rand(1, 5); // create an image object using the chosen background $image = imagecreatefromjpeg("cbg$bgNum.JPG"); $textColor = imagecolorallocate ($image, 255, 255, 255); // write the code on the background image imagestring ($image, 5, 5, 8, $rand, $textColor); // create the hash for the verification code // and put it in the session $_SESSION['ranval'] = md5($rand); [tab][/tab] // send several headers to make sure the image is not cached[tab][/tab] // Date in the past header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // always modified header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); // HTTP/1.1 header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: post-check=0, pre-check=0", false); // HTTP/1.0 header("Pragma: no-cache");[tab][/tab] // send the content type header so the image is displayed properly header('Content-type: image/jpeg'); // send the image to the browser imagejpeg($image); // destroy the image to free up the memory imagedestroy($image); ?>

This file is what will handle our image verification output. It has been commented so you can understand what each stage of the code is doing. This file will place a jpg image in to the header of this document when called. Also it encrypts the five letters and numbers written on the image and stores the encrypted value in the $_SESSION variable for later comparison. We can now call this image up by adding the following to any php document:

 

<img src="randomImage.php" width="200" height="40" />

But wait, we still need a background for our image verification. This has already been included in the above code. It will select a random image name out of cbg1.JPG, cbg2.JPG, cbg3.JPG, cbg4.JPG and cbg5.JPG (the extensions must be in upper case if you are using a Linux server). So if you open up paint and draw 5 images similar to these below. The dimensions need to be 60px wide and 30px high, then when we display the image we stretch it to 200px by 40px to distort the font of the text and help prevent it being automatically read.

 

Posted ImagePosted ImagePosted ImagePosted ImagePosted Image

 

There we go, now our image verification system is in place and we have finished our initial setup of the site and all our library files are in place.


================================================================

 

 

 

I will be writing the next part now, just need to divide this up a bit. Again, please no replies yet.

 



8ennett

Right I've asked for this tutorial to be deleted because there are too many problems with the code formatting and it keeps messing up. Instead I am going to put all the php files in to a rar file and upload to mediafire then you can just download them and read through the tutorial while opening the files yourself.


yordan

Moved the tutorial here in order to avoid MyCents loss.