Creating a PHP-Based Content Management System

来源:互联网 发布:数据噪声是什么意思 编辑:程序博客网 时间:2024/05/18 02:59

这是英语原文,在接下来的日子里,本人逐篇翻译

 

Creating a PHP-Based Content Management System, Part 1

 

Peter Zeidman


7/6/2004

(Editor's note: Intranet Journal is re-running some of our popular tutorials, including the following on a PHP-based CMS.)

Printer Friendly Version

If you're going to run an intranet site, then you'll probably want a content management system (CMS) — a tool used to organize documents and keep track of what's where. I've covered a plethora of such systems in previous articles, but for many businesses there can be only one solution: to design and implement their own custom system.

Why? It's not like the off-the-shelf systems lack features or stability. On the contrary, many have been crafted by hundreds of man-hours of work, and are successfully implemented by thousands of Web sites and intranets. But when it comes down to it, it's hard to have much clue as to how they work. If you want to customize the way these systems operate, you'll often have to wade through vast amounts of (often badly documented) code to find what needs changing.

Writing your own CMS, on the other hand, can lead to a solution that is better suited to your requirements, better addresses the needs of your users, and is better understood by your development team. If you have the time and expertise to write your own in-house system, it may well prove the better option. And this is what I shall be embarking upon in this series.

The system we create will be written using the PHP programming language, which excels in the development of Web-based systems. I'll be using MySQL as the database server, but the system will be written to allow the use of alternative databases, such as PostgreSQL or SQL Server.

So what will this system actually do? First and foremost, it will allow the bulk of the intranet or Internet site's content to be easily stored and managed in a database. We'll also include a number of other features required for running a successful site, such as authenticating users and managing files.

Some basic PHP knowledge will be needed for coding your own CMS, although most of what you'll need to know will be demonstrated here. I'll assume you have access to a server running PHP and a database system. Once the series is complete, I'll make available a polished version of the CMS for anyone to use.

I don't promise the vast array of abilities incorporated into systems such as Postnuke, Smarty, or some commercial content management systems. But just having lots of features isn't always what's needed, and this series will help you to develop a system specifically targeted to your needs. With that, let's get going...

Have a question about this article, object-oriented programming, or PHP? Visit Intranet Journal's Discussion Forum

Planning the CMS

To begin with, we'll plan how our PHP-based content management system will work. In subsequent articles, I'll demonstrate how each of the major components are implemented, leading to a complete system.

The first step is a basic specification of what our CMS must do. Obviously, this will depend on your needs:

 

  • Content Management: Probably the most vital function of the system, it must store content such as documents and news in a database, and display to the user whatever he or she requests. An easy-to-use interface is required to allow editors to add, remove, or modify content.

     

  • User authentication: There may be certain areas of the intranet or Internet site to which we wish to limit access. At the very least this will be the "admin" area, where the editor of the site will be able to add, edit or modify content. You may also wish to have areas only available to certain departments or staff.

     

  • Page uniformity/templates: The system should have a uniform look and feel, and this design element needs to be separated from the logic element, e.g., the programming required to display an article should be separated from how that article looks (stylistically) on the screen.

Object-Oriented Programming

PHP helps the design process by supporting object-oriented programming (OOP). When putting together our system, there are certain chunks of programming that are needed again and again, such as database access, user authentication, etc. To keep this code neat and tidy, we bundle it together in PHP files called "classes." We can then create instances (or "objects") of these classes whenever they are needed. Thus, the class can be thought of as a blueprint for one or more instances.

For example, we could create a class with code for connecting to a database, and then create an instance of that class whenever we need to query the database. If this isn't immediately clear then don't worry, it will become more obvious when we start coding. This method of programming allows a complex system to be broken down into smaller and simpler blocks, which makes life easier when it comes to management, modification, and error finding.

Let's now consider how the system will fit together. This will doubtless be tweaked as you consider the requirements for your own system, but below is a basic outline:

 

We have four main PHP modules (or "classes") that will be widely used in the system. These are tasked with accessing the database, allowing the user to upload files to the site, reading and writing templates, and logging users in and out. These classes all "extend" one parent class called "systemObject."

Think of these four as being independent of one another, yet all inheriting whatever data we put in systemObject. This technique of hierarchy allows us to make changes effecting all four system classes, just by adding or modifying the code in the systemObject parent class. Again, this concept will become clearer when we start coding. In the middle of the diagram are the basic areas of the administration system, and each will need one or more PHP pages to perform the required tasks.

Join me next month, when we'll start implementing the most important classes for the content management mystem. In the mean time, you may wish to familiarize yourself with object-oriented PHP programming, and consider the requirements for your own intranet site.

 

 

 

Creating a PHP-Based Content Management System, Part 2

 


 

 

Peter Zeidman


8/5/2004

Go to page: 1 2 

Printer Friendly Version

Have a question about this article, object-oriented programming, or PHP? Visit Intranet Journal's Discussion Forum

Last month I laid out the structure of a content management system (CMS), to be written in PHP for use on a Web site or intranet. This month we'll get going on writing the most basic code on which it will rely.

This article requires a basic knowledge of PHP programming, although a number of concepts are explained for those less experienced.

Our CMS will be stored in a number of folders, structured as follows:


 cmsadmin
 includes templates images (The admin area of the Intranet) (The PHP code that will be included in a number of different pages) (Templates for pages on the Intranet) (Pictures that will appear on the Intranet)

You may wish to create these four folders now. We're going to start by creating the PHP class which all others will "extend." This will be the root of the administration system, and anything we put in it (such as variables and functions) will trickle down to the other classes.

This root class will be called 'SystemComponent'. The code follows, and a full explanation is below:

<?php
class
SystemComponent {

var $settings;

function getSettings() {

// System variables
$settings['siteDir'] = '/path/to/your/intranet/';

// Database variables
$settings['dbhost'] = 'hostname';
$settings['dbusername'] = 'dbuser';
$settings['dbpassword'] = 'dbpass';
$settings['dbname'] = 'mydb';

return $settings;

}

}
?>

Reminder: A class is a block of code. Whenever we need to run that code, we create an 'object' or 'instance' of the class. We can create as many instances of a class as we like. If you don't understand objects and classes by the end of this article, I recommend getting a book or finding a Web site on Object Oriented Programming.

The above code starts off by telling PHP that our class will be called 'SystemComponent'. Between the braces (squiggly brackets) we declare the variable $settings, and a function called 'getSettings'. The purpose of this is to store a number of values in $settings, containing the path on the server to the intranet ('siteDir'), and the details of the database. Change these appropriately for the database system you'll be using (this tutorial uses MySQL, more details coming up). Finally, the 'return' command sends $settings to whichever class or function has requested it. We'll be storing more data in $settings as the series progresses.

Save this code to a file called SystemComponent.php in the 'includes' folder you created. Now let's do something with this class.

All of the information to be displayed in our Content Management System will be stored in a database. It is sensible, therefore, to create a reusable PHP class that we can call upon whenever we need to access our data. The code listed here is for connecting to a MySQL database. If you'll be using a different system, such as PostgreSQL, MS SQL or SQLite, then change the code appropriately. It's obviously quite a bit longer than our previous class, but it performs a number of very important tasks. The code follows:

<?php
////////////////////////////////////////////////////////////////////////////////////////
// Class: DbConnector
// Purpose: Connect to a database, MySQL version
///////////////////////////////////////////////////////////////////////////////////////

require_once 'SystemComponent.php';

class DbConnector extends SystemComponent {

var $theQuery;
var $link;

//*** Function: DbConnector, Purpose: Connect to the database ***
function DbConnector(){

// Load settings from parent class
$settings = SystemComponent::getSettings();

// Get the main settings from the array we just loaded

$host = $settings['dbhost'];
$db = $settings['dbname'];
$user = $settings['dbusername'];
$pass = $settings['dbpassword'];

// Connect to the database
$this->link = mysql_connect($host, $user, $pass);
mysql_select_db($db);
register_shutdown_function(array(&$this, 'close'));

}

//*** Function: query, Purpose: Execute a database query ***
function query($query) {

$this->theQuery = $query;
return mysql_query($query, $this->link);

}

//*** Function: fetchArray, Purpose: Get array of query results ***
function fetchArray($result) {

return mysql_fetch_array($result);

}

//*** Function: close, Purpose: Close the connection ***
function close() {

mysql_close($this->link);

}


}
?>

Some explanation is required. After we've named the class 'DbConnector', we state 'extends SystemComponent'. This tells PHP to grab all of the data and functions from SystemComponent, and provide us with access to them (we'll need this in order to get the $settings variable we created earlier).

The first function, 'DbConnector', has the same name as the class that contains it, meaning it's run automatically when DbConnector loads. It firstly calls the 'getSettings' function we wrote earlier, and extracts from it the various database settings. It then uses these settings to connect to the database. (Note that we have no code to deal with errors, this will be covered in detail next time.)

The other functions are explained below:

FunctionPurpose   queryExecute a database queryfetchArrayCreate an array containing each record found using the 'query' function (above)closeCloses the database connection. The register_shutdown_function command in the DbConnector function ensures this happens automatically when the object is no longer in use.

Save the above code (also attached at the bottom of this article) to the 'includes' folder, with the name DbConnector.php. This class will be widely used in the Intranet system, so let me give you an example of how we'd create an instance of DbConnector, extract some data, and display it to the user. Let's imagine that our database stores the details of one customer, and we want to get hold of his / her name and display it. Here's the code:

<?php

// Get the PHP file containing the DbConnector class
require_once('DbConnector.php');

// Create an instance of DbConnector
$connector = new DbConnector();

// Use the query function of DbConnector to run a database query
// (The arrow -> is used to access a function of an object)
$result = $connector->query('SELECT firstname FROM customers');

// Get the result
$row = $connector->fetchArray($result);

// Show it to the user
echo $row['firstname'];

?>

If you'd like to try out the DbConnector class now, you'll need to save the above code in the includes folder in a php file, and set up a 'customers' table in your database. I'll be covering the set up of our Intranet's database next time.

The importance and power of using a database is clear — we can store information in a formal way, and rapidly access, manipulate and change it. The information we extract or store is specified using the 'query' function of the DbConnector class, and we create instances of DbConnector using the 'new' command, as shown above. This also demonstrates the usefulness of classes — if the settings are changed in SystemComponent, then all of the classes that extend it will automatically be changed.


Next month we'll be adding code to deal with errors, and creating the first part of the administration system that will allow you to add or remove information on the intranet. Until then!

 

 

Creating a PHP-Based Content Management System, Part 3

 


 

 

Peter Zeidman


9/7/2004

Go to page: 1 2 

Printer Friendly Version

Have a question about this article, object-oriented programming, or PHP? Visit Intranet Journal's Discussion Forum

 

In this series we've been working through the construction of a Content Management System, for use with an Intranet or Web site. The foundation has been laid in the form of a PHP class for accessing the database ('DbConnector'), and to kick off this month we'll set up the database itself, and create the first working part of the system.

Creating the Database

The first table we're going to add to our database will store articles, for display on the Intranet. The ability to share information is the most important function of an Intranet, and the job of the Content Management System is to make doing this as easy as possible. Consider your own data requirements, a few important ones spring to mind for most articles tables:

 Field PurposeType   IDA unique number given to each article, and the primary key of the table.IntegerTitleThe title of the articleVarchar(300)TaglineA very short summary of the articleVarchar(600)SectionThe category to which the article belongsIntegerTheArticleThe article itselfText

Before we can create the system itself, we need to create the database to store our information. The code below will set this up if you're using the MySQL database system - uses of other systems should modify the commands appropriately. Copy and paste the following into the MySQL admin tool, or use one of the many free 'client' programs available:

CREATE TABLE `databasename`.`cmsarticles` (
`ID` int(6) unsigned NOT NULL auto_increment COMMENT 'The unique ID of the article',
`title` varchar(200) NULL COMMENT 'The article title',
`tagline` varchar(255) NULL COMMENT 'Short summary of the article',
`section` int(4) NULL DEFAULT 0 COMMENT 'The section of the article',
`thearticle` text NULL COMMENT 'The article itself',
PRIMARY KEY (`ID`)
);

If all has gone to plan, you should now have a working table in the database. We're now going to create a page to allow you or your staff to enter articles into the system.

Creating the editor

Firstly, design a form using the HTML editor of your choice. Create text fields for each database field (excluding ID). An example is below:

 Title:

 Tagline:

 Section:

 Article:

Set the action of the form to be newArticle.php (with the method 'post'), and save this page in a folder called cmsadmin (described in the previous article). If any of this is unclear, just browse through the attached file at the end of the article. Note that the 'section' field is currently a text box, by the time we've finished it'll be a drop-down list, allowing you to choose a section of the site in which to place the article.

Next, we'll create the PHP code to deal with whatever is typed into this form, and save it to the database for later retrieval. The code is below, with explanation beneath:

<?php
// Get the PHP file containing the DbConnector class
require_once('../includes/DbConnector.php');

// Check whether a form has been submitted. If so, carry on
if ($HTTP_POST_VARS){

// Create an instance of DbConnector
$connector = new DbConnector();

// IMPORTANT!! ADD FORM VALIDATION CODE HERE - SEE THE NEXT ARTICLE

// Create an SQL query (MySQL version)
$insertQuery = "INSERT INTO cmsarticles (title,tagline,section,thearticle) VALUES (".
"'".$HTTP_POST_VARS['title']."', ".
"'".$HTTP_POST_VARS['tagline']."', ".
$HTTP_POST_VARS['section'].", ".
"'".$HTTP_POST_VARS['thearticle']."')";

// Save the form data into the database
if ($result = $connector->query($insertQuery)){

// It worked, give confirmation
echo '<center><b>Article added to the database</b></center><br>';

}else{

// It hasn't worked so stop. Better error handling code would be good here!
exit('<center>Sorry, there was an error saving to the database</center>');

}

}
?>

We start off by requiring the 'dbConnector' class that we created in the previous article. If it can't be found, an error will be displayed. We then check whether a form has been submitted, by seeing if $HTTP_POST_VARS exists (this variable contains all the submitted form data). Next we assemble the database query, and store it in $insertQuery, before actually running it using the query command we created last time. Finally, a message is shown to the user confirming success, or showing failure.

Try adding an article. For the time being you'll have to type an integer into the 'section' box, as we haven't yet created a drop-down menu to display the section names. We now have a way of adding articles to the database, but for this to be of any use we must allow people to retrieve them again. Let's make a page to do that.

To demonstrate extracting information from the database, we'll provide users with a way to view articles by selecting from a list of titles. This may be useful on the front page of your Intranet site, to show a list of the top 5 newest articles. Here's the code:

<b> WHAT'S NEW: </b><br>
<?php

// Require the database class
require_once('includes/DbConnector.php');

// Create an object (instance) of the DbConnector
$connector = new DbConnector();

// Execute the query to retrieve articles
$result = $connector->query('SELECT ID,title FROM cmsarticles ORDER BY ID DESC LIMIT 0,5');

// Get an array containing the results.
// Loop for each item in that array
while ($row = $connector->fetchArray($result)){

echo '<p> <a href="viewArticle.php?id='.$row['ID'].'">';
echo $row['title'];
echo '</a> </p>';

}
?>

The above snippet of code will get the ID number and title of the five newest articles from the database, and loop through each of them displaying them on separate lines. If you wish to save this, save it as index.php in the root folder (i.e. the one above cmsadmin).

For each headline displayed by the code above, there's a different link to viewArticle.php, a page which we'll create shortly. The idea is that viewArticle.php?id=1 will display the article with the ID 1, viewArticle.php?id=2 will show article 2, etc etc. Here's the code for viewArticle:

<?php
// Require the database class
require_once('includes/DbConnector.php');

// IMPORTANT!!! Validate the ID number. See below

// Create an object (instance) of the DbConnector
$connector = new DbConnector();

// Execute the query to retrieve the selected article
$result = $connector->query('SELECT title,thearticle FROM cmsarticles WHERE ID = '.$HTTP_GET_VARS['id']);

// Get an array containing the resulting record
$row = $connector->fetchArray($result);

?>

Your selected article: <?php echo $row['title'];?>
<br><br>
<?php echo $row['thearticle'];?>

 

And you're done. The database is queried, using $HTTP_GET_VARS to extract the ID number from the link (eg viewArticle.php?id=253). Each piece of data can then be 'echoed' where required on the page.

We've done well - our system allows editors to add information to the site, and display it to the user in a variety of ways. It still can't be called a fully fledged Content Management System, but we're getting there.

Something very important to note, sufficiently important for me to reach for the bold button in my editor. At the moment there's an enormous security flaw in this script, because we're not doing something called validation. We expect the user to provide viewArticle.php with an ID number, so an article can be extracted and all shall work well. But what if they're here to make trouble, and rather than an ID number they provide some malicious code designed to do damage? I'll cover in detail how to protect ourselves from this next time, so don't use this system for real until you've read the next article!! If you wish to get started before then, look up how to do form validation using PHP.

That's all for now, until next time!

 

 

Creating a PHP-Based Content Management System, Part 4

 


 

 

Peter Zeidman


10/8/2004

Go to page: 1 2 

Printer Friendly Version

Have a question about this article, object-oriented programming, or PHP? Visit Intranet Journal's Discussion Forum

When Keats wrote "There is not a fiercer hell than the failure in a great object," he probably wasn't referring to the objects that make up the content management system we've been developing in this series. A testament to his forethought, if a component of our system fails then it won't be a good thing, which is why this month we'll be adding some rudementary code for validation and error handling. We'll then go on to creating the categorization system for articles stored on the system.

Validation

There are two main reasons for validating a user's input, the first being security. Let me give you a quick example of how things can go wrong.

Let's say I have a query that searches the database for a value provided by the user. The query code would look something like:

$thequery = 'SELECT * FROM products WHERE name = "'.$HTTP_POST_VARS['uservalue'].'"';
$connector->doQuery($thequery);

The query is compiled from a string (SELECT...), and the user's input ($HTTP_POST_VARS...), before being executed. If the user searches for "waffles," then the query executed will be:

SELECT * FROM products WHERE name = "waffles"

And all is well in the world. However, if your system is accessed by someone a little less pleasant (such as a 13-year-old hacker on school vacation), malicious code could be inserted in place of the word "waffles." The best way to protect your system against this type of attack (called query injection) is to check that all input from the user is in the format you expect. It's difficult, if not impossible, to be 100 percent safe, but we can do our best.

The second reason for validation is convenience and error handling. For instance, if you have a form asking for a telephone number, you don't want the user to be able to insert letters by mistake. Likewise, you may wish to check that dates have been entered correctly, or a ZIP/ postal code is valid.

The Solution: a Validator Class

The idea is to create a Validator class that we can call everytime we need to deal with user input. It'll check whether the given input is safe and correct, and if not it will display an error.

To begin with, we'll create the framework for a 'Validator' class:

<?php
require_once
'SystemComponent.php';
class Validator extends SystemComponent {

     var $errors; // A variable to store a list of error messages
     ...
}
?>

There are a few basic types of data we may need to validate:

  •  General: Just check something was typed in
  •  Text Only (i.e., no punctuation or other symbols allowed)
  •  Text Only and no white spaces allowed
  •  E-Mail addresses
  •  Numbers
  •  Dates

We'll need to write a method for each of these. (A method is a chunk of code that performs a task, which we put inside the class.) I'll give an example of one here, the rest can be found in the source file at the end of the article.

function validateNumber($theinput,$description = ''){

if (is_numeric($theinput)) {

return true; // The value is numeric, return true

}else{

$this->errors[] = $description; // Value not numeric! Add error description to list of errors
return false; // Return false

}

}

This method is very simple. It takes the data from the user as input (storing it in $theinput), as well as a message to display if validation fails. It then tests $theinput, and if it's a number returns "true." This will tell our system to move along and not get concerned. If the value turns out not to be numeric, the error message is stored in the variable $errors, which we created in the previous code snippet, above.

Once we've created methods for each of the data types required, only two more methods are needed. The first will allow us to check whether any errors have occurred, and the second returns a list of errors (if there were any). These are both quite straight-forward, and can be found in the source file at the end of the article.

So how do we use our new class with a form? It's simple. Let's say we've created a form to add an e-mail address to a mailing list. There are two text boxes in the form — one for the user's e-mail address, the other for the maximum number of messages they wish to receive per week. Here's how it works:

<?php
// Gather the data from the form, store it in variables
$userEmail = $HTTP_POST_VARS['email'];
$maxMessages = $HTTP_POST_VARS['maximum'];

// Create a validator object
require_once('includes/Validator.php');
$theValidator = new Validator();

// Validate the forms
$theValidator->validateEmail($userEmail, 'Email Address');
$theValidator->validateNumber($maxMessages, 'Maximum number of messages');

// Check whether the validator found any problems
if ($theValidator->foundErrors() ){

// The were errors, so report them to the user

echo 'There was a problem with: '.$theValidator->listErrors('<br>'); // Show the errors, with a line between each

}else{

// All ok, so now add the user to the mailing list

}

?>

By checking that the user's input was valid, we've reduced a number of security risks, prevented incorrect entries in our database, and helped the user if they've forgotten to fill out any part of the form. We can now integrate this into all of our Intranet's forms, and move on to some more creative stuff...

Getting Organized

We've covered how to create forms, manipulate the database, and set up a page for creating articles. But having an intranet with hundreds of articles will quickly become disorganized and unwieldy. The solution is to create a system of categories (sections), into which each article or news item can be stored. You'll notice that when we set up the database in Part 3 we created a "section" field. Well, now we'll use it.

We want to be able to dynamically add and remove sections from our system, so we'll need a new table in the database. Here's the schema:

 Field PurposeType   IDA unique ID for each sectionIntegernameThe name of the sectionVarchar(20)parentidIf this is a sub-section, the id of the parentInteger

The SQL code to create this table is listed below:

CREATE TABLE `database`.`cmssections` (
`ID` int(4) unsigned NOT NULL auto_increment COMMENT 'The unique ID of the section',
`name` varchar(20) NULL COMMENT 'The section name',
`parentid` int(4) NULL DEFAULT 0 COMMENT 'The ID of the parent section',
PRIMARY KEY (`ID`)
);

(If you don't know how to run SQL queries, see a short explanation in the Intranet Journal Discussion Forum, SQLCourse.com, or consult a reference text on SQL.)

In the same way that we created an admin page for adding articles, it's simple to create a page to add, edit or delete sections. We want the page to look something like the one below:

 

SECTION 1  - Delete
SECTION 2  - Delete

Create a Section:

 Name:

 Parent:

When this page loads, a list of sections is displayed as well as a link to remove any of them. There's also a form to create a new section. Notice the drop-down list for choosing the "parent" section; a similar menu will be used on the Add Article page to choose which section an article belongs in.

How's all this coded? We begin by connecting to the database, and creating a Validator object (using the class we just designed):

<?php
// Require the classes
require_once('../includes/DbConnector.php');
require_once('../includes/Validator.php');

// Create an object (instance) of the DbConnector and Validator
$connector = new DbConnector();
$validator = new Validator();

 

Next, we'll add code to deal with the "delete" link. Notice that we use the Validator to make sure the ID number is numeric. To tell the page whether it should be adding, deleting or just listing the sections, we set the 'action' variable in the query string (i.e., http://yourintranet/cmsadmin/sectionEdit.php?action=XXX). This variable is read by the first line below:

if ($HTTP_GET_VARS['action'] == 'delete'){

// Store the ID of the section to be deleted in a variable
$sectionID = $HTTP_GET_VARS['id'];

// Validate the section ID, and if it's ok then delete the section
if ( $validator->validateNumber($sectionID,'Section ID') ){

// The validator returned true, so go ahead and delete the section
$connector->query('DELETE FROM cmssections WHERE ID = '.$sectionID);
echo 'Section Deleted <br><br>';

}else{

// The validator returned false, meaning there was a problem
echo "Couldn't delete. There was a problem with: ".$validator->listErrors();

}

}

The code to insert a section is quite simple so I won't detail it here, you'll find it easy to understand by reading the source file at the end. The last part of the PHP code for this page is to list the sections as shown above, with the delete link along side:

// Execute the query to retrieve articles
$result = $connector->query('SELECT ID,name,parentid FROM cmssections');

// Get an array containing the results.
// Loop for each item in that array
while ($row = $connector->fetchArray($result)){

echo $row['name'].' - &nbsp;&nbsp; '; // Show the name of section
echo '<a href="editSections?action=delete&id='.$row['ID'].'"> Delete </a>'; // Show the delete link
echo '<br>'; // Show a carriage return

}
?>

And now we have a category system set up. You can use it to define the sections that will make up your site, and then assign articles to them. On the front end that your users will see, you could have a page called showarticles.php and use it to show only the articles in a particular category (e.g., showarticles.php?id=1 for news, showarticles.php?id=2 for press releases, etc.).

Next month I'll be covering authentication and security, so we can keep unwanted people out of the admin area. I'll be providing you with the rest of the forms required to make up the admin area, and will then move on to tying up the project — leaving you with a fully functional content management system for your intranet. Until next time!

Creating a PHP-Based Content Management System, Part 5

 


Peter Zeidman


11/8/2004

Go to page: 1 2 

Printer Friendly Version

Have a question about this article, object-oriented programming, or PHP? Visit Intranet Journal's Discussion Forum

Welcome to the penultimate installment of the series. So far we've looked at the basics of database interaction using PHP, as well as some vital techniques such as validation and error handling. We've allowed anyone to be able to add or remove content at the touch of a button, without programming knowledge. However, we've been rather to liberal in allowing 'anyone' to make changes. We need to keep certain areas of the site, such as the admin system, private. Sadly, few people will respect a "keep out" sign, and this month we'll be creating a class that'll act as a guard for the more private areas of your Intranet.

I should emphasize that this is a very basic type of security, and techniques such as secure servers, secure data transmission and encryption are not covered by this article. These should be investigated if you're planning on storing any sensitive information.

 Note: An updated copy of DbConnector.php, created earlier in the series, is required and is included with this article. There are two changes: blank lines after the closing ?> have been removed, and new functions have been created for extracting the last query used (helpful for debugging), and for returning the number of results found by a query (thanks to forum member w0lf for suggesting that).

Let us now consider what we want our system to do:

  •  Store users' details in the database
  •  Group users into categories, for instance administrators, editors, and staff, in order of security access
  •  Only allow groups of users access to specific areas of the site

The first bit we'll need to secure is the admin area, used for adding and removing content on the site. To get started, we'll set up database tables to store our user information.

 Table: GroupsColumnPurpose  IDID of the groupgroupnameName of the group

 

 Table: UsersColumnPurpose  IDID of the useruserA unique username for the userpassThe user's password, encryptedthegroupGroup to which the user belongsfirstnameThe user's first namesurnameThe user's surnameenabledA 1 or a 0 specifies whether the
user is enabled, allowing you to
block troublesome ones

SQL queries for creating the above two tables are below, which can be run in any SQL client:

# Create groups table

CREATE TABLE `cmsgroups` (
`ID` int(4) unsigned NOT NULL auto_increment,
`groupname` varchar(15) default NULL,
PRIMARY KEY (`ID`)
) TYPE=MyISAM;

# Create 10 groups, where 1 has the highest security

INSERT INTO `cmsgroups` VALUES (1,'Admin');
INSERT INTO `cmsgroups` VALUES (2,'Editors');
INSERT INTO `cmsgroups` VALUES (3,NULL);
INSERT INTO `cmsgroups` VALUES (4,NULL);
INSERT INTO `cmsgroups` VALUES (5,NULL);
INSERT INTO `cmsgroups` VALUES (6,NULL);
INSERT INTO `cmsgroups` VALUES (7,NULL);
INSERT INTO `cmsgroups` VALUES (8,NULL);
INSERT INTO `cmsgroups` VALUES (9,NULL);
INSERT INTO `cmsgroups` VALUES (10,'Anonymous');

# Create user table

CREATE TABLE `cmsusers` (
`ID` int(4) unsigned NOT NULL auto_increment,
`user` varchar(20) default NULL,
`pass` varchar(20) default NULL,
`thegroup` int(4) default '10',
`firstname` varchar(20) default NULL,
`surname` varchar(20) default NULL,
`enabled` int(1) default '1',
PRIMARY KEY (`ID`)
) TYPE=MyISAM;

# Create sample user

INSERT INTO `cmsusers` VALUES (1,'admin',PASSWORD('admin'),1,'Mr','Admin',1);

So how should we go about securing our site? We're going to write a class called Sentry to check whether a user is logged in. The system we're going to rely on is called sessions, a method of storing a user's details for the duration of their visit to the website or Intranet.

The constructor function (the function executed when the class is created) is as follows:

function sentry(){

session_start();
header("Cache-control: private");

}

This simply tells PHP to start the session, and adds a header that stops the password being stored in the user's cache. The function to logout is equally simple:

function logout(){

unset($this->userdata);
session_destroy
();
exit();

}

This destroys the variable containing the user's details, the session data, and prevents further code from being executed. We next create a function to check whether the user is already logged in, and optionally to actually perform a login. The function has a number of parameters:

function checkLogin($user = '',$pass = '',$group = 10,$goodRedirect = '',$badRedirect = ''){
...
}

We pass the username and password checkLogin, and if either of these are incorrect then the page should redirect to the address stored in $badRedirect. If $user and $pass are correct, we redirect to $goodRedirect. $group is used to specify the minimum group level that's allowed to access this resource, we'll specify that group level 10 has the least security privileges, group 1 has the most. If checkLogin finds that the user is already logged in, it should confirm that the original username and password provided are still valid.

Our first clause in checkLogin takes a look at whether a user seems to be logged in, by checking whether the session variables already exist:

// User is already logged in, check credentials
if (
$_SESSION['user'] && $_SESSION['pass']){

// Validate session data
...

// Look up the user in the database by performing and SQL query
$getUser = $loginConnector->query("SELECT * FROM cmsusers WHERE user = '".$_SESSION['user']."' AND pass = '".$_SESSION['pass']."' AND thegroup <= ".$group.' AND enabled = 1');

// Redirect to goodRedirect or badRedirect appropriately
...

Notice in the above code, we use the PASSWORD function in the SQL query. For those of you not familiar with this, here's how it works. When we originally create the user's record in the database, we don't store the plain password; instead we use the PASSWORD function to encrypt it. What is now stored in the database is an apparently random string of letters and numbers, and the original password can never be recovered (hopefully). When we then come to check a login, we perform the same jumbling function on the provided password, and compare the result with the string of letters stored previously. If they're the same, then the original passwords match, and the user is authenticated.

The next piece of code is used when a user hasn't previously logged in:

}else{

// Validate the input

...

// Lookup the user in the DB

$getUser = $loginConnector->query("SELECT * FROM cmsusers WHERE user = '$user' AND pass = PASSWORD('$pass') AND thegroup <= $group AND enabled = 1");
$this->userdata = $loginConnector->fetchArray($getUser);

if ($loginConnector->getNumRows($getUser) > 0){

// Login OK, store session details

$_SESSION["user"] = $user;
$_SESSION["pass"] = $rowUser["pass"];
$_SESSION["group"] = $rowUser["thegroup"];

// Redirect if goodRedirect was provided

...

} else {

// Login BAD, Destroy session data
unset($this->userdata);

// Redirect to badRedirect if appropriate
return false;

}

}

If more than one result is found in the database, i.e. the user's details were correct, the username, password and group are stored in the session. OK, We're pretty much done. Let's now test our class by creating a login form and a test page we want to restrict.

 

Creating the Login Form

We're going to create a form to allow the user to login using the Sentry class we created on the previous page. You can create this using any HTML editor, I include a simple one in the source files at the end. We want it to look something like:

Login


 User:

 Pass:

Logout 

 

Make sure the form's action is set to the filename of the page containing it (e.g., login.php), and the method is set to post. The PHP we put on this page is simple:

<?php
require_once
("../includes/Sentry.php");

$sentry = new Sentry(); // Create a sentry object

// Check the user's submitted login is valid
if
($HTTP_POST_VARS['user'] != ''){

$sentry->checkLogin($HTTP_POST_VARS['user'],$HTTP_POST_VARS['pass'],10,'welcome.php','failed.php');

}

// Log out the user
if ($HTTP_GET_VARS['action'] == 'logout'){

$sentry->logout();

}
?>

And that should all work nicely. The final step is to secure a page. For the purposes of the demonstration we'll create a page called welcome.php, that just says "welcome to the admin area." We only want people in groups 1 and 2 (admin and editors) to be able to access it, so at the start of the file we put:

<?php
require_once
('../includes/Sentry.php');
$theSentry = new Sentry();
if (!$theSentry->checkLogin(2) ){ header("Location: login.php"); die(); }
?>

And hey presto, your page is secure. Make sure you include that code on every page you want to protect.

Once you have these basics in place, creating a fully fledged user management system isn't far away. You can create pages to allow for the automatic signup, editing, deleting and emailing of your members. Samples of all of these functions will be included in the final part of the series, which you can find here next month.

Until next time!