How to Create a Forum in PHP from Scratch

In this tutorial we’ll see how to make a forum using PHP and MySQL. We have to cover a lot of different things, so let’s start!

Some details about this tutorial

  • You can download a compressed folder with the whole project inside. So sometimes I won’t show all the code of a file in order to focus on the important parts of the project.
  • The code I’ll show is exactly the same than the one you can download, except for some comments. In the original project you’ll have everything well documented (using phpDocumentor).
  • I’ll ommit the <?php and ?> tags in the tutorial, but every time you type PHP code in a file you should put it between them.
  • This project doesn’t follow a MVC pattern, but we’ll use classes and try to separate the different functionalities.

What and how

What are we going to do? We have to think the answer very carefully, and not only “a forum”, also the functionality we want to offer.

Answer: We are going to develop a forum where anyone can write a post without registration and other people can see it and write a response.

How are we going to do that? Here we have to think about how we are going to organize the project, the files..etc

Answer: As you know we are going to use PHP and MySQL. The files are going to be organized by the code they implement, so a class file will be in a different folder than a CSS file, for instance.

Designing and Building the Database

When we start to code a project, the first thing we usually do is to think about the database. Because that decision is going to be very important, and a lot of the PHP code depends on it.

There are many options here, we could make a table of posts and another of users, even three tables if we want, but we’ll take the easiest way in order to focus on the PHP code.

DB Schema

This table is all we need, at least for a simple forum like ours.

So a thread is going to consist of several rows in the table. The first post of a thread is going to have the permalink_parent set to NULL, and the rest of them are going to have the permalink_parent with the permalink of their parent, as the name suggests.

To create the table we have to execute the following sentence in our database:

CREATE TABLE `posts` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `title` varchar(128) COLLATE utf8_bin NOT NULL,
  `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `content` text COLLATE utf8_bin NOT NULL,
  `permalink_parent` varchar(128) COLLATE utf8_bin DEFAULT NULL,
  `author` varchar(128) COLLATE utf8_bin NOT NULL,
  `permalink` varchar(128) COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `upermalink` (`permalink`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

The lengths of some fields depend on how many people are going to use the forum, but these values are enough for the moment.

Note that we are defining an UNIQUE KEY over the permalink column, which means that in our forum a title must be unique, I know we usually can post a couple of threads with the same title in other forum, however for the sake of simplicity we are keeping the title as unique.

File Organization

As well as designing the database we need to think about how we are going to organize the project, instead of starting to code without a previous plan.

The title and permalink are going to be unique, so we have to check if a title is taken.

At this point we can also take different approaches, we should try to keep things as simple as we can. So this is a little schema of our final project.

File Schema

As you can see there are many files, but all of them are necessary if we want to keep things organized. Let’s see them step by step, and try to get the techniques we’ll use.

The media folder

This tutorial is focus on the PHP side, so I won’t explain too much about the CSS and the HTML code, you can download all the code at the end of this tutorial, so don’t worry. Anyway I’ll try to summarize the content of this folder.

  • style.css. It’s the CSS file for all the forum, includes the reset CSS code and the rest of the styles like a regular CSS file.
  • bg_container.png. The background of the main container.
  • bg_body.png. The background of the body element.

The Configuration File

This file is going to be useful from almost every script of the project. There are different options to implement it, but probably the most used is just a file with some define() lines, like this:

define('DB_HOSTNAME','hostname');
define('DB_USERNAME','user');
define('DB_PASSWORD','password');
define('DB_NAME','forum');

define('RE_PUBLIC_KEY','Your recaptcha public key');
define('RE_PRIVATE_KEY','Your recaptcha private key');

I’ll explain later the two last lines. So from now on every time we include the file we can get the value of a constant with something as simple as this:

require_once 'config.inc.php';

echo DB_HOSTNAME; //output: hostname

Header and Footer

There is a part of the HTML code that isn’t going to change so we can write once and include the file every time we need it. This is very simple and it will save us time and lines of code.

Header

The header.html file will have all we need to build the first part of a page: the doctype tag, the html, head, the beginning of the body element, the link to the CSS file…

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<meta http-equiv="Content-Type"content="text/html; charset=UTF-8">
	<title>TutToaster Forum [Demo]</title>
	<link href="media/style.css" media="all" rel="stylesheet" type="text/css" />
	<!--[if !IE 7]>
	<style type="text/css">
		#wrap {display:table;height:100%}
	</style>
	<![endif]-->
</head>
<body>
<div id="wrap">
	<div id="header_wrap">
		<div id="header">
			<h1><a href="index.php">TutToaster Forum [Demo]</a></h1>
		</div><!-- End #header-->
	</div><!-- End #header_wrap-->
	<div id="main">

As you can see the #main and #wrap divs, the body and html tags are open so we have to close them in the footer.

Footer

The footer.html file is very similar, we’ll include it when we finish the output to “close” the page.

	</div> <!-- End main-->
</div>  <!-- End Wrap-->
<div  id="footer_wrap">
	<div id="footer">
		<div id="footer_right">
			<p>Forum made by Jose for TutToaster. You can see the whole tutorial <a href="http://35.226.78.216/how-to-create-a-forum-in-php-from-scratch">here</a>.</p>
		</div> <-- End footer_right-->
	</div> <!-- End footer-->
</div> <!-- End footer_wrap-->

<script type="text/javascript&quot; src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript">
	$('input, textarea').click(function(){
		$(this).select();
	});
    if ($("#message_header p").html()) {
		setTimeout(function() {
			$("#message_header").slideUp('slow');
		}, 5000);
	}

We are loading the jQuery at the bottom of our page, which is a good practice, because the rest of the site will be rendered before we execute any jQuery code.

The jQuery code is basically to improve the interface, so when a user clicks on an input field all the text will be selected. The if works in this way: when we see the p element (in #message_header) has something, then we hide it with the slideUp effect after 5 seconds. You can see this code working in the live demo after you try to post a message with an empty field or if the title you have selected is already taken.

The difference between require and include is the include() construct will emit a warning if there is an error (script goes on) and require() will emit a fatal error (script stops). As you may guess, adding _once to those statements we prevent the file from being included twice. Let me show you the final result with something very simple like this:

require_once 'header.html';

echo "A very simple message";

require_once 'footer.html';

Header and footer with a simple message

Great! Now we have our “template” and all we need to output will be shown between the header and the footer code as long as we include these files.

The Database class

In order to separate the PHP code that is going to execute SQL queries and the rest of PHP code we’ll make a database class, this class will connect to the database, get some rows and save data.

If we think about it, if an object always need to connect to the database, the connection code must be in the constructor. So every time we create an object that object is going to be ready to execute queries with the function mysql_query().

Also, the methods we’ll need are going to depend on the rest of code, but here I show you the beginning of the class as it will be in the end of the development:


require_once 'config.inc.php';

class Database
{

	private $db;

	function __construct()
	{
		// We set our own error_handler
		set_error_handler(array($this,'errorHandler'));

		//Connect to Database
		$this->db = mysql_connect(DB_HOSTNAME, DB_USERNAME, DB_PASSWORD);
		mysql_set_charset('utf8', $this->db);
	 	mysql_select_db(DB_NAME, $this->db);
	}

	function errorHandler($errno, $errstr)
	{
		switch ($errno){
		    case E_WARNING:
				echo '<b>There has been an error with the MySQL database connection. '.
				  	 'Please, make sure the config file is OK.</b>';
				die();
		    default:
		        return false;
		}
	}

	function getThreads()
	{
		$query = "SELECT p.title, date_format(p.date,'%m/%d/%Y %T') as date, p.permalink, p.author ".
				 "FROM posts p ".
				 "WHERE permalink_parent IS NULL";
		return mysql_query($query);
	}

	// ... other useful methods ....

}

Probably the most important method is savePost, so let’s take a look at it apart from the previous ones.

function savePost($input, $permalink, $newThread)
{

	$postOK = $this->isValidPost($input, $permalink, $newThread);

	if (!$postOK) {
		return -1;  // something missing
	}

	if ($newThread) {
		$query = 'INSERT IGNORE INTO posts '
				. '(title, content, author, permalink, permalink_parent) VALUES'
				. '("%s","%s","%s","%s", NULL)';

		$query = sprintf($query,
			clean(strip_tags($input["title"])),
			clean(strip_tags($input["content"])),
			clean(strip_tags($input["author"])),
			$permalink);
	} else {
		$query = 'INSERT IGNORE INTO posts '
				. '(content, permalink_parent, author) VALUES'
				. '("%s","%s","%s")';

		$query = sprintf($query,
			clean(strip_tags($input['content'])),
			$permalink,
			clean(strip_tags($input['author'])));
	}

	if (mysql_query($query)) {
		return 1;   // Ok
	} elseif (mysql_errno() == 1062) {
		return -2;  // Duplicated title error
	} else {
		return -3;  // DB error
	}
}

Note when we create the $query string, we use sprintf to make the code cleaner.

The method isValidPost is private and its code is:

private function isValidPost($input, $permalink, $newThread)
{
	if ($newThread) {
		return !empty($input['title'])
			&& !empty($input['content'])
			&& !empty($input['author'])
			&& !empty($permalink);
	} else {
		return !empty($input['content'])
			&& !empty($input['author'])
			&& !empty($permalink);
	}
}

The different numbers we return in the savePost method have a meaning (we’ll see it later), but, basically we have to recognize different kind of errors to show a different message each time, so we can’t return just false, and the use of exceptions could be complicated for the beginners.

This is actually a classic approach to manage the database from the PHP code, we’ll see at the end of the tutorial what people are using now and why we should use it too. But this class is enough for us and I assume readers know a little bit about PHP probably also know this way to connect to the database and manage queries.

Validation Helper

This script (helpers/validation.php) is just a group of functions we’ll use all over the project. There are 3 functions here:

String to Permalink (strToPermalink)

Takes a string as input and creates a human-friendly URL string. Its implementation is:

function strToPermalink($str)
{
	$permalink = iconv('UTF-8', 'ASCII//TRANSLIT', $str);
	$permalink = preg_replace("/[^a-zA-Z0-9\/_|+ -]/", '', $permalink);
	$permalink = strtolower(trim($permalink, '-'));
	$permalink = preg_replace("/[\/_|+ -]+/", '_', $permalink);

	return $permalink;
}

To sum up all we do here is to remove “weird” characters like á or é and others, and then convert -, spaces and others into _. (Permalink is also known as slug or even permanent link).

Clean input (clean)

Here we take care of removing slashes if magic_quotes_gpc is enabled and escaping some characters with mysql_real_escape_string. Don’t worry if you don’t know what magic quotes are or why we code this function, when we talk about security you’ll understand it.

function clean($value)
{
	// Stripslashes
	if (get_magic_quotes_gpc()) {
		$value = stripslashes( $value );
	}

	// Quote if not a number or a numeric string
	if (!is_numeric($value) && !empty($value)) {
		$value = mysql_real_escape_string($value);
	}
	return $value;
}

Process post (processPost)

This function has a lot of lines (compare to the rest of this file), I’ll try to explain all of them without code:

  1. Prepare the array to be returned with all values initialized as NULL
  2. If it has everything in the array to start, then checks if the Recaptcha answer is OK, generates the permalink, tries to save the post and returns a message.
  3. If the input array is not empty but it hasn’t got all we need OR the Recaptcha input field is wrong, then it returns an error message.
  4. Finally, the showBox variable in the array is set. The box we’ll be shown if there was an error, or always in case we are in the page where users can write a response.

I’ll explain Recaptcha later, now let’s focus on the rest of the code. The array it will return is initialized in this way:


/* This is not neccesary but it makes code cleaner,
and we could change this new array and without modifying
the original $_POST */
$input = $_POST;

$returnArray = array('errorHTML' => NULL,
					 'okMessage' => NULL,
					 'recaptchaError' => NULL,
					 'showBox' => NULL);

Then we code the steps 2 and 3 of the list we did before.

if (array_sum($input) > 0 && !empty($input["recaptcha_response_field"])) {
	//We have something in the $input and the Recaptcha response
	$resp = recaptcha_check_answer (RE_PRIVATE_KEY,
		$_SERVER["REMOTE_ADDR"],
		$input["recaptcha_challenge_field"],
		$input["recaptcha_response_field"]);
	if ($resp->is_valid) {
		require_once 'classes/Database.php';
		$db = new Database();

		if (is_null($permalink)){
			$permalink = strToPermalink(strip_tags($input['title']));
		}

		$saveResult = $db->savePost($input,
									clean(strip_tags($permalink)),
									$newThread);

		switch ($saveResult) {
			case -1:  //A field is empty
				$returnArray['errorHTML'] = 'All fields are required';
				break;

			case -2: //The title is already in use
				$returnArray['errorHTML'] = 'Duplicated title, please, choose another one.';
				break;
			case -3: //There has been an error with the query
				$returnArray['errorHTML'] = 'There has been an error with the Database, '.
										'please, try again later.';
				break;

			default: //Everything is OK
				$returnArray['okMessage'] = '<p>The post has been published. You can see it '
					.'<a href="view_thread.php?permalink=' . $permalink . '">here</a></p>';
		}
	} else { //The recaptcha response is not valid.
		$returnArray['recaptchaError'] = $resp-&gt;error;
	}
} elseif (array_sum($input) &gt; 0) {
	//We have something in the $input array but the recaptcha response is not
	$returnArray['errorHTML'] = 'All fields are required';
}

You can see here the different kind of errors we return and the reasons. Finally we set the showBox variable of $returnArray:

if ($newThread) {
	$returnArray['showBox'] = !empty($returnArray['errorHTML'])
	|| !empty($returnArray['recaptchaError'])
	|| empty($input);
}

The View Class

There is a part of the HTML code that depends on some parameters that come from PHP. To keep things as clear as we can we are going to make a class exclusively for this purpose.

The home page is going to show a list of threads, so let’s code that method and the constructor of our new class:

// we need the class db to make an object
require_once 'Database.php';

//we'll also need the recaptcha helper later
require_once 'helpers/recaptcha.php';

class View{

	private $db;

	function __construct()
	{
		$this->db = new Database();
	}

    function tableThreads()
	{
		$content = "";

		if (($threads = $this->db->getThreads()) && mysql_num_rows($threads) > 0) {
			 $content .= '<h1>Threads</h1>';
			 $content .= '<table border="0" width="" id="posts_list">';
			 $content .= '<tr>';
			 $content .= '<th class="title">Title</td>';
			 $content .= '<th>Date</td>';
			 $content .= '<th>User</td>';
			 $content .= '</tr>';

			while ($row = mysql_fetch_assoc($threads)) {
				$content .= '<tr class="thread">';
				$content .= '<td class="title">';
				$content .= '<a href="view_thread.php?permalink=';
				$content .= htmlspecialchars($row['permalink']) . '">'.$row['title'].'</a>';
				$content .= '</td>';
				$content .= '<td class="date">'.htmlspecialchars($row['date']).'</td>';
				$content .= '<td class="author">'.htmlspecialchars($row['author']).'</td>';
				$content .= '</tr>';
			}
			$content .= '</table>';
			return $content;
		} else {
			return false;
		}
	}

The htmlspecialchars function provides a way to change some HTML elements into entities, so every time we are going to output something from the database we should use this function. We’ll se more about this later.

The next method we’ll code is composeTable, and it will be useful to show all the posts of a thread.

// ... previous code ...

private function composeTable($post, $firstPost, $numRows)
{
	$htmlTable = "";

	if ($firstPost)
		$htmlTable .= '<h1>'.htmlspecialchars($post['title']).'</h1>';

	$htmlTable .= '<table border="0" width="895">';
	$htmlTable .= '	<tr>';
	$htmlTable .= '		<th>Message</th>';
	$htmlTable .= '		<th>Date</th>';
	$htmlTable .= '		<th>Author</th>';
	$htmlTable .= '	</tr>';
   	$htmlTable .= '	<tr>';
	$htmlTable .= '		<td class="title">'.htmlspecialchars($post['content']).'</td>';
	$htmlTable .= '		<td class="date">'.htmlspecialchars($post['date']).'</td>';
	$htmlTable .= '		<td class="author">'.htmlspecialchars($post['author']).'</td>';
	$htmlTable .= '	</tr>';
	$htmlTable .= '</table>';
	if ($firstPost && $numRows &gt; 1)
		$htmlTable .= '<h1>Responses</h1>';

	return $htmlTable;
}

function tableThreadContent($permalink)
{
	$content = "";

	if ($posts = $this->db->getContentThread($permalink)) {
		$num_rows = mysql_num_rows($posts);
		if ($num_rows > 0) {
			while($row = mysql_fetch_assoc($posts))
				$content .= $this->composeTable($row,
					is_null($row['permalink_parent']),
					$num_rows);
		}
		return $content;
	} else {
		return false;  //database error
	}
}

[/html]

<p>The second method goes around all the posts in a thread and composes the HTML with the first one (composeTable). </p>

<p>The method composeTable is private because we'll only call it from the tableThreadContent method in the same class and its functionality is only useful inside this class. In the future if we want to make a class that extends this one and uses that method all we need to do is change private for protected.</p>

<p>Now let's think about what happens if we don't have a single thread. Apart from being very sad it could be a problem if we don't show a warning message. This is a very simple method to do that:</p>

// ... previous code ...
function htmlError($from_view_thread = false)
{
	if ($from_view_thread) {
		//From view_thread.php
	   	$html = '<p class="error">There is no thread with this title. Sorry! ';
		$html .= 'You can go back to <a href="index.php">the main page</a>.</p>';
	}else{
		// From index.php
	   	$html = '<p class="error">There aren\'t any threads. Sorry! </p>';
	}
	return $html;
}

If we call the method from view_thread.php it means we are in a thread that doesn’t exist, so we show a different message in that case.

Now we need to create a form for users who want to send new posts (responses or the beginning of a new thread).

Recaptcha or how to avoid spam

At this point we have to think about the spam problem. As you can see there is no registration process so anyone can just fill the form and post a message with the content they want every time they like. How to handle the input will be discussed later, but the problem here is a bot could fill the form and submit it.

How can we avoid that? Well, the easiest and probably most used solution is a captcha system. Yes, those boring images of characters we have to identify and replicate. Specifically the implementation bought by Google, Recaptcha.

But what are the guarantees? Well, Twitter uses it in the sign up process and if we forum doesn’t grow too much (actually like Amazon or Google) we won’t have more problems. The question now is: how can we use it?

Recaptcha provides a file (in our project is helpers/recaptcha.php) and there we have all the functions we need, actually even more. You can see the documentation at the the end of the tutorial.

Finally, the other two methods are messageBox and buttonPostThread. The first one will return a string with the HTML needed to compose a post (a form) when we need to and the second one just returns a div to go to post_message.php and create a new post.

// ... previous code ...
function messageBox($new, $recaptchaError = null, $errorHTML = null)
{
	$content = '<div>';
	if (!empty($errorHTML)) {
		$content .= '<div><p class="error">' . $errorHTML . '</p></div>';
	}
	if ($new) {
		$content .= '<h1>Post a Message</h1>';
	} else {
		$content .= '<h1>Post a Response</h1>';
	}
	$content .= '<form action="" method="post" accept-charset="utf-8">';
	if ($new) {
		$content .= '';
	}
	$content .= '';
	$content .= '<textarea name="content" rows="8" cols="88">Message</textarea>';
	$content .= recaptcha_get_html(RE_PUBLIC_KEY, $recaptchaError);
	$content .= '';
	$content .= '</form>';
	$content .= '</div>';

	return $content;
}

The $new parameter indicates if the form will be shown in post_message.php to send a new thread or in view_thread.php (to send a response). This will helps, for instance, to ask for the title of the thread if it’s going to be a new one or not.

The $recaptchaError is what we need to render properly the box with the captcha and the input, because in case the user types wrong the code the recaptcha_get_html will take care of showing a message.

Finally, the $errorHTML could contain an string of an error and we have to show it here. (We’ll see later what kind of errors could happen).

The last method will be buttonPostThread, and as I’ve said before its only functionality will be to return a string, but the reason we make it is to keep consistent the idea of not writing HTML code in some scripts like index.php or view_thread.php, besides of that the code of these pages will be very clear if we keep that idea in mind.

// ... previous code ...
function buttonPostThread()
{
	return '<div class="newThread">'
		  .'<a href="post_message.php">Create a new thread</a>'
		  .'</div>';
}

Probably you are thinking: “why all this code if we haven’t written a script which shows anything directly?”. And it’s OK you wonder that, but now you will see the advantages of having a modular and independent code.

Home page

Let’s start with our first page, index.php. These are the things we have to show:

  • Header
  • List of threads OR a warning if there aren’t threads
  • Link to make a new thread
  • Footer

The code is as simple as this list:

require_once 'classes/View.php';
require_once 'header.html';

$view = new View();

if ($htmlString = $view->tableThreads()) {
	echo $htmlString;
} else {
	echo $view->;htmlError();
}

echo $view->buttonPostThread();

require_once 'footer.html';

The output in the browser with 3 threads is:

Home

Create a new post

The post_message.php script is going to show a form, get the input and try to process this input as a new post. If everything is ok we’ll show the link to the new post, otherwise we’ll show an error message.

require_once 'classes/View.php';
require_once 'helpers/validation.php';
require_once 'header.html';

$result = processPost();

if ($result['showBox']) {
	$view = new View();
	echo $view->messageBox(true, $result['recaptchaError'], $result['errorHTML']);
} else {
	echo $result['okMessage'];
}

require_once 'footer.html';

The processPost function is implemented in helpers/validation.php and, as I’ve explained before, processes the input, tries to save the post and return an array with different values. This is the output in Firefox:

Post Message

View Thread

The view_thread.php script will combine a couple of things.

  • We have to get some posts, the initial one first, and show them properly.
  • The form to write a response is going to be almost the same than the one we did to write a new thread. (Hint: We’ll use the same function).

The first thing we do is to include all the files we need and clean the input ($_GET) with a function (implemented in the helpers/validation.php file).

require_once 'classes/View.php';
require_once 'helpers/validation.php';
require_once 'header.html';

$view = new View();
$permalink = clean($_GET['permalink']);

Here we process the $_POST array and create a “message box”, a form to send a response to the thread.

// .. previous code ...

$postInput = $_POST;

$result = processPost($permalink, false);

$messageBox = $view->;messageBox(false,
	$result['recaptchaError'],
	$result['errorHTML']);

Finally we print the posts and the form to write a new response. In case we get a false value from messageBox we’ll show an error because there isn’t a thread with that permalink.


if ($htmlString = $view->tableThreadContent($permalink)) {
	echo $htmlString;
	echo $messageBox;
} else {
	echo $view->htmlError(true);
}

require_once 'footer.html';

So all these parts together compose our view_thread.php script. Here we have the final result with a thread and a response to the first post:

View Thread

MySQL from PHP alternatives

We have used some functions to manage the database like:

mysql_connect();
mysql_set_charset();
mysql_select_db();
mysql_query();

The reason I’ve used these functions is because they are well known by the most of novice PHP developers, but you should know these functions have lost their popularity. I’ll explain you why.

Actually, there are many reasons, the mysql functions provide a procedural interface, doesn’t support a lot of things in the new versions of MySQL (newer than 4.1.3) and have more security problems. So take a look at the alternatives:

MySQL Improved (mysqli)

This extension was developed to take advantage of new features found in MySQL systems versions 4.1.3 and newer. Some features are:

  • Object-oriented interface
  • Support for Prepared Statements
  • Support for Transactions

Apart from that is more secure than the mysql classic extension because we can use bound parameters (details in links at the conclusion).

PHP Data Objects (PDO)

The main difference is this API can be used with any kind of database (in theory), that means it doesn’t have to be MySQL. So the change of the database system in the project it’s going to be easier.

But like yin yang there is a disadvantage, some advanced new features of new MySQL versions are not supported. Anyway is usually better use this or the previous alternative than the classic mysql extension.

Comparative from official documentation

Comparative of MySQL extensions

The details of how to use mysqli or PDO are in the official documentation.

The security issue

First of all I have to say I’m not a security expert but if you are going to make a web app you need to know the possible security holes, and try to prevent attacks, not being a hacker doesn’t mean you don’t need to know about security. So let’s see the most common attacks in a web app, and how to prevent them in our forum.

Cross Site scripting (XSS)

XSS exists when your PHP code outputs some data that has neither been filtered nor escaped. Some code like this is vulnerable to XSS:

echo $_POST['var1'];

That means someone could send whatever they want in a form and will be shown later, and that’s very dangerous. The simplest thing they can send is:

alert('This will be shown instead of a real var1!!')

So the the code will be executed by the browser. A more dangerous use, could be extract cookie content with javascript and log in a system using that cookie. So, now we have understood it’s dangerous, how can we prevent it?

We have to escape data which comes from users. In this forum we have made a couple of things. First of all we strip the HTML and PHP code with strip_tags(), and when we show data from database we escape using htmlspecialchars(). Let’s see some code:

// The user sends this:
// $_POST['email'] = "alert('fool!')";
// $_POST['name'] = "<strong>Hercules</strong>";

// Now we remove all the HTML and PHP code
$email = strip_tags($_POST['email']);

echo $email; //Output: alert('fool')
echo $name; //Output: Hercules

As you can see the script tags are not shown so the alert will not be executed. But Hercules wanted to show his name with strong tags, and actually it’s harmless, but we removed those tags too. A possible solution is to provide a list of our own tags and then replace them with the actual HTML elements. We haven’t implemented this, but it would be very easy:

$string = strip_tags($_POST['name']);
$string = str_replace('[strong]','<strong>');
$string = str_replace('[/strong]','</strong>');

The other thing we do is to use htmlspecialchars(), this is redundant because with strip_tags HTML code will be removed and not inserted into our database, but there are some characters which do nothing so we can convert them into HTML entities. Remember it’s usually better to a be a little bit paranoid when we talk about security.

// From database
$result = "alert('ups!')";

$resultOK = htmlspecialchars($result);

echo $resultOK; //Output: alert('ups!')

The output is exactly the string, but some characters are actually HTML entities, so the code is not executed, just shown in the browser.

SQL injection

This vulnerability is, as the name suggests, when someone injects SQL code into your web app. Here we have to take care of a couple of details, but let’s see the main problem.

We have a query with some variables from $_POST, something like:

$user = $_POST['user'];
$password = $_POST['password'];

$query = "SELECT name, age, credit_card
		  FROM users
		  WHERE username = '$user' AND password = '$password' ";

What if someone sends: ‘ or 1=1 ; — as the password? The query would be:

SELECT name, age, credit_card
FROM users
WHERE username = 'jose' AND password = '' or 1=1 ; -- '

So the WHERE condition is always true because (1=1) is, and (false) || (true) is also true. Consequence? The query selects all the records.

A simple way to prevent this is to use the function mysql_real_escape_string() which escapes special characters in a string taking into account the current character set of the connection so that it is safe to place it in a mysql_query(), as we do in the clean function (helpers/validation.php file).

But still there could be a problem with slashes and magic_quotes. To summarize magic_quotes is something annoying and we have to deal with it. Magic quotes is a deprecated PHP configuration that, when turned on it automatically escapes characters for you.

The problem is sometimes we don’t want that, so in our forum when we clean the input we use stripslashes() in case magic_quotes is turned on, which un-quotes a quoted string, and from that point we escape the data. (see clean function at the beginning of this tutorial). So with this implementation we can insert O’connor, and it won’t be a problem, without this consideration, the input could be inserted as O\’Connor into the database.

There could be a problem with the magic_quotes_sybase configuration, but, apart from being deprecated, it’s turned off by default so usually it won’t be a problem, and in our forum we don’t deal with it.

If we had used other alternative to manage the database from PHP, a lot of these precautions wouldn’t be necessary, you have more details in the official documentation.

Cross-Site Request Forgery (CSRF)

According to Wikipedia, is a type of malicious exploit of a website whereby unauthorized commands are transmitted from a user that the website trusts. But we don’t have to worry about that since our forum is open to anyone who wants to write and there aren’t sign up and log in processes. Anyway at the conclusion you have some links in case you want to know more about this exploit.

How to set up your own forum

When you download the code, you have to follow the next steps to have your own forum working:

  • Get a Recaptcha key here.
  • Create a database with the SQL query at the beginning of the tutorial.
  • Change the values of constants in the config.inc.php file with yours.
  • And, of course, put the files where you can access with PHP and Apache working (localhost’s directory).

Conclusion

I have to admit this tutorial is long but if you read it a couple of times and take a look at the code I’m sure you’ll be able to understand all the project.

The organization of code in classes and files is up to the developer. And as long as we don’t write the same code over and over again, or just type queries, html and PHP code in the same file, the project will be ok. I mean, everything about make more or less classes or create a specific function is not a science. It’s more a design problem, so feel free to think about other ways to develop this forum, it will help you to be a better developer.

For instance I could have written a post class, or make everything procedural, but this was just an approach, and you can see the power of this in how many lines we have written in our index or post_message web pages. In addition, if we need more features the development will be very easy since everything is well documented in the files you can download.

There is other feature we could implement, to set the value of the inputs to the content of $_POST if it’s received, but that could make the code the code less clear, so I’ve avoid it in order to make things easier.

Here you have some links to some sites where you can learn more about security, PHP and MySQL.

This entry was posted on Monday, August 9th, 2010 at 10:46 and is filed under Tutorials. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

I'm from Spain. I've been working in an Internet company for more than a year, I love everything about Internet, from designing to PHP.

About the author Published by Jose

28 Responses »

  1. Not a bad tutorial for a beginner level project.

    There’s just one thing I’d like to mention.. your use of set_error_handler is a somewhat poor choice.

    It is assumed in the code that the errors are only caused by your MySQL related code. However, set_error_handler places a *global* error handler, which means that any code causing an E_WARNING level error will produce output which claims there’s a problem with the MySQL connection.

    It would be a better idea to handle mysql errors on a case by case basis (check return value of call)

    Ps. stop using die() for error handling. Read here why: http://codeutopia.net/blog/2010/07/28/the-do-x-or-die-pattern-must-die/

    • Hi Jani,

      set_error_handler saved me more explanations, but you’re right, any warning error will pass through that function, which couldn’t be enough clear.

      About using die(), well, it’s because the same reason, if I had used exceptions I’d have to explain them, making the tutorial longer. You can see a lot of examples in the official documentation using die(), instead of something more correct, probably for the same reason.

      Anyway it’s ok you mention that, it could help readers.

      Thanks for the comment!

    • You could have used trigger_error, which is almost as simple as die() – but yeah, I understand what you mean 🙂

  2. Great job Jos�!

    Could you possibly add the code for “Views”, “Replies”, “Last post” in the forum overview? That would be great!

    Thanks,
    Robert

    • Hi Robert,

      thanks for the comment. Unfortunately, I can’t, sorry.

      This is a tutorial and everything is done, but you can always try it, and that’d help you to improve your skills.

      If you have any question in the process, please, let me know and I’ll help you.

      Regards!

  3. I like this tutorial, but have to mention something. Your database class is for this table only. I think that it would be better if you build a universal class with CRUD method for any table.

    But great stuff for beginners.

    • Hi,

      yes, that would be better, but if I’d built an universal class, the code would be longer, since I had to pass more parameters to the methods in that class, and the tutorial is long enough for a beginner.

      Anyway it’s useful you note that, I’ll take it into consideration for future tutorials.

      Thanks for the comment!

  4. When i post a response I get There has been an error with the Database, please, try again later 🙁 thanks

  5. Hey guys where can i download his files from this page??? i dont know how? thanks

  6. Jose,

    First off, thanks for this. I had been looking for something like this for weeks. I tried to go in and remove all the Recapchta requirements and I believe I found the majority of them. The issue I’m having is that when I submit a post nothing happens. I just get a blank screen. I think it has something to do with the processPost() because there was quite a bit of recaptcha stuff in there. Do you have any idea what I could check? Thanks.

  7. please am using dreamwaver with PhPMyAdmin.. what i get is There aren’t any threads. Sorry!

    please kindly help me out.

    thanks

  8. thanks for such a good information…

  9. I cannot find the files to download, can anyone tell me where they are?

  10. i get Input error: k: Format of site key was invalid

  11. i get this Input error: k: Format of site key was invalid
    please help me

  12. Input error: k: Format of site key was invalid

  13. Hi!
    First of all, thanks for a great tutorial! It was very helpful!

    I’ve got everything to work, but I have one question. If you enter your comments and then enter reCaptcha wrong, everything you entered in the comments field will be lost. Are there any solutions for this problem?

    Thanks!

  14. This is a good start. I was looking for a tutorial creating a forum from scratch. Thank you. 🙂

  15. Where can i download the source?

  16. Very nice tutorial for begineer..

  17. I have a forum, I would like to understand the programming, and I love the step by step with explanation… but, WHERE IS THE COMPRESSED FOLDER WITH THE WHOLE PROJECT INSIDE? The link to the “Take the source files” only reloads the page.

  18. where is download link jone,

  19. Im not sure where to put the code should it be in the mysql or the php designer?

  20. how to create Captchar Image

Trackbacks

  1. Web Mee