Configuration Manipulation With PHP Config

Use the PEAR Config class to read and write configuration files in XML, PHP and INI formats.

Space And Time

One of the most important tasks when building software applications is gaining a clear understanding of which parts of the application can be configured by the user, and which parts cannot. For example, if you're building a generic tool to administer databases, you would obviously want the DBA to be able to configure the application for different databases by inputting the database names and access parameters for each. However, you might not want to give the DBA control over other aspects of the application - for example, the protocol version used for communication with the RDBMS, or the method of RDBMS file access - as these would be internal to your code and also perhaps too complex for user-level configuration.

Although making such decisions might appear trivial and largely a matter of common sense, they are no small task, especially for complex applications - deciding which aspects of your software are configurable by the user is something that requires a thorough understanding of both the software's goals and the end-user requirements it is built to address, and it can take even experienced developers a fair amount of soul-searching to get it right.

Once the configuration variables within your application have been identified, it is traditional to separate them from the standard variable namespace and place them in a separate area, such that they may be manipulated independently. Most often, these variables are placed in one or more configuration files, and a user interface is built to interact with these variables, to read them, and to modify them as required.

That's where this article comes in. Over the next few pages, I'm going to introduce you to a toolkit designed specifically for manipulating configuration files, thereby reducing the number of lines of code needed to read and modify application variables. This toolkit is written in PHP, one of my favourite languages, and implemented as a class, suitable for use in any PHP-based application and accessible via standard OO techniques. Flip the page, and let me introduce you to Config.

Plug And Play

The Config class is brought to you by PEAR, the PHP Extension and Application Repository (http://pear.php.net). In case you didn't know, PEAR is an online repository of free PHP software, including classes and modules for everything from data archiving to XML parsing. If you need a widget to perform a specific task, and you don't feel like writing it yourself, take a look at PEAR - chances are good that someone's already done the hard work for you.

In case your PEAR distribution didn't include Config, or if you chose not to install the PEAR component, you can get yourself a copy from the official PEAR Web site, at http://pear.php.net/ - simply unzip the distribution archive into your PEAR directory and you're ready to roll!

Very simply, Config provides application developers with a set of APIs that ease the task of reading, writing and maintaining application configuration files. As a tool designed to assist in the manipulation of data, it is capable of producing configuration files in a variety of formats, including Windows INI, XML and PHP formats.

If you're in the business of building Web applications, and if those applications require some amount of configuration to get up and running, you're going to find Config invaluable to your development cycle. Written as a PHP class, Config can easily be integrated into any PHP-based Web application, and can substantially reduce the amount of time you spend manipulating application configuration files and data. You'll find it functional, powerful and (if you're the kind who likes fiddling) easily extensible, and it'll soon be a standard component of every application you write.

There's only one problem with Config - it could do with better documentation. The documentation available on the PEAR Web site is a little too terse for novices, and can throw up more questions than answers if you're coming at it cold. Hopefully, though, the next few pages will help resolve that problem, by illustrating how powerful the class really is with some simple examples.

Your Friendly Neighbourhood Hulk

Now that the hard sell is over and you're (hopefully) all set up with Config, let's take a simple example to see how it works.

Let's assume that I have a PHP associative array containing a series of configuration variables, as in the following snippet:

<?php
$mailConfig = array(
        'name' => 'Bruce Banner',
        'email' => 'hulk@angry.green.guy',
        'host' => 'mail.apollo.domain'
);
?>

Normally, these configuration values would arrive via user input, perhaps via a form, perhaps through command-line entry. For simplicity and ease of understanding, however, I'm hard-wiring them for the moment; once you've understood the basics, a subsequent example will demonstrate how they can be extracted from a form.

The task at hand now consists of storing them permanently, by saving them to a configuration file. With the Config class, this is a snap - consider the following script, which demonstrates:

<?php

// configuration data
// hard-wired right now
// usually taken from form input
$mailConfig = array(
        'name' => 'Bruce Banner',
        'email' => 'hulk@angry.green.guy',
        'host' => 'mail.apollo.domain'
);

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig($mailConfig, "PHPArray");

// write configuration to file as PHP array
$c->writeConfig("mail.conf", "PHPArray");

?>

Let's dissect this a little to see how it works.

  1. The first step is, obviously, to include all the relevant files for the class to work.
// include class
include("Config.php");

Once that's done, I can safely create an object of the Config class.

// instantiate object
$c = new Config();

This object instance will serve as the primary access point to the data in the application configuration file(s), allowing me to do all kinds of nifty things with it.

  1. Next, the object's parseConfig() method is used to read the configuration data into the object.
// read configuration data and get reference to root
$root =& $c->parseConfig($mailConfig, "PHPArray");

An interesting point to note is the second argument to parseConfig(); this is a predefined type that tells the Config object the format or structure in which the configuration variables are stored, thereby simplifying the task of extracting those variables and restructuring them as per the object's internal requirements. The Config object defines a number of such formats, covering most of the major types of configuration files - here's a list of the important ones (there are a couple more, take a look at the manual for more details):

  1. "GenericConf" - a generic configuration file consisting of colon-delimited variable-value pairs, such as the following:
mouse:Mickey
duck:Donald
  1. "IniFile" - a Windows-style initialization (INI) file, such as the following:
[characters]
mouse=Mickey
duck=Donald
  1. "PHPArray" - a PHP associative array, such as the following:
<?php
$mouse = 'Mickey';
$duck = 'Donald';
?>
  1. "XML" - an XML file, such as the following:
<?xml version="1.0" encoding="ISO-8859-1"?>
<characters>
  <mouse>Mickey</mouse>
  <duck>Donald</duck>
</characters>

The second argument passed to parseConfig() thus tells the Config object how the configuration variables are stored, thereby activating the rulesets needed to read and manipulate them.

  1. Once the configuration data has been read into the object via parseConfig(), you can use it for whatever purpose you wish - to retrieve an individual value (or set of values) from it, to modify values, or to write the data to a file. Coincidentally, the script above picks door number three, writing the configuration data to a file named "mail.conf" in the current directory via the writeConfig() method.
// write configuration to file as PHP array
$c->writeConfig("mail.conf", "PHPArray");

Note that here too, the second argument to writeConfig() is a type identifier - this tells Config what format the data should be written in. As you will see shortly, altering this parameter significantly affects the format of the configuration file.

When you run this script via your Web browser, the configuration data in the PHP array $mailConfig will be written to a configuration file named "mail.conf", as noted previously. If you look into this file, you should see something like this:

<?php
$name = 'Bruce Banner';
$email = 'hulk@angry.green.guy';
$host = 'mail.apollo.domain';
?>

If you don't like this, the writeConfig() method also offers you a further degree of customization when dealing with the PHPArray type - you can specify that the variable-value pairs be structured as an associative array of key-value pairs (rather than regular variables, as above) by adding an optional third argument to writeConfig(). Here's how:

<?php

// configuration data
$mailConfig = array(
        'name' => 'Bruce Banner',
        'email' => 'hulk@angry.green.guy',
        'host' => 'mail.apollo.domain'
);

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig($mailConfig, "PHPArray");

// set options for output array
$options = array('name' => 'mailSettings');

// write configuration to file as PHP array
$c->writeConfig("mail.conf", "PHPArray", $options);

?>

In this case, the data written to "mail.conf" will be structured as an associative array with the name $mailSettings. Here's what the file would look like:

<?php
$mailSettings['name'] = 'Bruce Banner';
$mailSettings['email'] = 'hulk@angry.green.guy';
$mailSettings['host'] = 'mail.apollo.domain';
?>

Different Strokes

I noted, on the previous page, that altering the second argument to writeConfig() significantly affects the format of the configuration file. Let's look at that a little more closely, by trying out the different variants. Consider the following rewrite of the previous example, which writes the configuration data as a generic configuration file instead of a PHP structure:

<?php

// configuration data
$mailConfig = array(
        'name' => 'Bruce Banner',
        'email' => 'hulk@angry.green.guy',
        'host' => 'mail.apollo.domain'
);

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig($mailConfig, "PHPArray");

// write configuration to file as generic conf file
$c->writeConfig("mail.conf", "GenericConf");

?>

Here's what it looks like:

name:Bruce Banner
email:hulk@angry.green.guy
host:mail.apollo.domain

As with the PHPArray type, you can customize the output format by specifying the characters to be used for comments, delimiters and line endings. So, for example, if you wanted the variable-value pairs separated by commas, you could use this script,

<?php

// configuration data
$mailConfig = array(
        'name' => 'Bruce Banner',
        'email' => 'hulk@angry.green.guy',
        'host' => 'mail.apollo.domain'
);

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig($mailConfig, "PHPArray");

// set options for delimiters
$options = array('equals' => ',');

// write configuration to file as generic conf file
$c->writeConfig("mail.conf", "GenericConf", $options);

?>

and the output would change to look like this:

name,Bruce Banner
email,hulk@angry.green.guy
host,mail.apollo.domain

You can write your configuration data in Windows INI file format with the following script,

<?php

// configuration data
$mailConfig = array(
        'name' => 'Bruce Banner',
        'email' => 'hulk@angry.green.guy',
        'host' => 'mail.apollo.domain'
);

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig($mailConfig, "PHPArray");

// write configuration to file as INI file
$c->writeConfig("mail.conf", "IniFile");

?>

which produces this output:

name=Bruce Banner
email=hulk@angry.green.guy
host=mail.apollo.domain

And finally, you can even write configuration data using the XML standard, with the XML type (note that this requires the presence of two other XML classes from PEAR, the Parser class and the Util class in order to work). Here's the script,

<?php

// configuration data
$mailConfig = array(
        'name' => 'Bruce Banner',
        'email' => 'hulk@angry.green.guy',
        'host' => 'mail.apollo.domain'
);

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig($mailConfig, "PHPArray");

// write configuration to file as XML file
$c->writeConfig("mail.conf", "XML");

?>

and here's the output:

<?xml version="1.0" encoding="ISO-8859-1"?>
<name>Bruce Banner</name>
<email>hulk@angry.green.guy</email>
<host>mail.apollo.domain</host>

If you know a little bit about XML, you'll know that is not really well-formed XML, since the root element is missing. This can be easily rectified; just add an option specifying the name of this root element to the writeConfig(), as done earlier with the PHPArray type. Here's the revised script,

<?php

// configuration data
$mailConfig = array(
        'name' => 'Bruce Banner',
        'email' => 'hulk@angry.green.guy',
        'host' => 'mail.apollo.domain'
);

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig($mailConfig, "PHPArray");

// set root element
$options = array('name' => 'mailSettings');

// write configuration to file as XML file
$c->writeConfig("mail.conf", "XML", $options);

?>

and the revised, well-formed output:

<?xml version="1.0" encoding="ISO-8859-1"?>
<mailSettings>
  <name>Bruce Banner</name>
  <email>hulk@angry.green.guy</email>
  <host>mail.apollo.domain</host>
</mailSettings>

Array Of Hope

So that takes care of writing configuration data to a file - now, how about reading it in?

Reading configuration data in from a file is accomplished via the parseConfig() method discussed previously - the only difference is that where earlier you handed the method a PHP data structure, here you pass it a file name (together with the second argument indicating the type of file). Once the data has been read in, a number of other methods become available to access the various key-value pairs.

Consider the following sample XML configuration file,

<?xml version="1.0" encoding="UTF-8"?>
<mysql>
    <host>localhost</host>
    <user>joe</user>
    <pass>secret</pass>
    <db>db456</db>
</mysql>

and this next PHP script, which takes care of reading it and converting the data within it to a native PHP structure:

<?php

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig("mysql.conf.xml", "XML");

// convert data into PHP array
print_r($root->toArray());

?>

Here's the output:

Array
(
    [root] => Array
        (
            [mysql] => Array
                (
                    [host] => localhost
                    [user] => joe
                    [pass] => secret
                    [db] => db456
                )

        )

)

What happens here is pretty simple - the parseConfig() method reads the XML configuration file, parses it and places it into a private member of the object, and simultaneously exposes a root object that can be used to interact with the configuration variables. This root object comes with a toArray() method, which can be used to convert the XML configuration data into a native PHP associative array (there's also a toString() method that can be used to convert the data structure into a neatly-formatted string for display).

If you want the value of a specific configuration variable (rather than the whole caboodle), it is simple to obtain it from the associative array created via toArray(),by drilling down to the appropriate key. Consider the following revision of the example above, which illustrates by retrieving the various configuration values and using them to connect to a MySQL database:

<?php

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig("mysql.conf.xml", "XML");

// convert data into PHP array
$config = $root->toArray();

// extract configuration data
$user = $config['root']['mysql']['user'];
$pass = $config['root']['mysql']['pass'];
$host = $config['root']['mysql']['host'];
$db = $config['root']['mysql']['db'];

// open connection to database
$connection = mysql_connect($host, $user, $pass) or die("Unable to connect!");
mysql_select_db($db) or die("Unable to select database!");

// execute query
$query = "INSERT INTO stocks (symbol, price) VALUES ('HYYJ', 198.78)";
$result = mysql_query($query) or die("Error in query: $query. " . mysql_error());

// close database connection
mysql_close($connection);

?>

OOP purists will be pleased to hear that there's also a more elegant method to retrieve data from a configuration file - more on that on the next page.

Up A Tree

In addition to the technique described previously, the Config class also includes a number of built-in methods to retrieve configuration values from the internal stack after they have been read in. In order to understand this alternative approach, you first need to conceptually understand how the Config class deals with a configuration file, and with the various sections within it.

Seen through the eyes of Config, every configuration file can be broken down into four basic elements:

  1. Sections - blocks within the configuration file, each with a name, containing one or more directives.

  2. Directives - variable-value pairs

  3. Comments - explanatory notes which can be ignored, these are meant only for human consumption

  4. Blanks - blank lines, useful for cleaning up the visual appearance of a configuration file

The root object returned by the parseConfig() method is a section, and as such, it exposes a number of different methods. For the moment, we'll focus on the getItem() method, used to return a reference to another section, to a directive, or to a comment. Once a reference is obtained to a directive or comment, other methods - the most common one is getContent() - become available to retrieve the content of that directive or comment.

In order to better understand how this works, consider the following example, which does exactly what the previous example did, but uses object methods and properties to retrieve the various configuration values.

<?php

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig("mysql.conf.xml", "XML");

// get reference to <mysql> element of file
$mysqlSection =& $root->getItem("section", "mysql");

// get reference to <host> element
$hostDirective =& $mysqlSection->getItem("directive", "host");
// get content of <host> element
$host = $hostDirective->getContent();

// get reference to <user> element
$userDirective =& $mysqlSection->getItem("directive", "user");
// get content of <user> element
$user = $userDirective->getContent();

// get reference to <pass> element
$passDirective =& $mysqlSection->getItem("directive", "pass");
// get content of <pass> element
$pass = $passDirective->getContent();

// get reference to <db> element
$dbDirective =& $mysqlSection->getItem("directive", "db");
// get content of <db> element
$db = $dbDirective->getContent();

// open connection to database
$connection = mysql_connect($host, $user, $pass) or die("Unable to connect!");
mysql_select_db($db) or die("Unable to select database!");

// execute query
$query = "INSERT INTO stocks (symbol, price) VALUES ('HYYJ', 198.78)";
$result = mysql_query($query) or die("Error in query: $query. " . mysql_error());

// close database connection
mysql_close($connection);

?>

If you've ever used the Document Object Model (DOM) in XML, the above probably looks strangely familiar to you. It should - the Config class uses a similar approach, providing methods to obtain references to different "nodes" of the configuration "tree", and thereby to retrieve the values of those nodes. Like the DOM, there are also methods to add, edit and delete nodes - but those come later.

A quick note on the getItem() method above - it can be used to return a reference to any section of the configuration file and requires, as first argument, one of the keywords "section", "directive", "comment" or "blank" (to identify the type of data needed) and, as second argument, an identifier to help it locate the necessary node. A number of different options can be sent as additional arguments to getItem() to help it identify the correct node - take a look at the manual for detailed information on this.

Once a reference to the correct node has been obtained, the getContent() method can be used to retrieve its value. This method may be used with both directives and comments.

Here's another example, this time using a Windows INI file like the one below,

[Settings]
POPAccount=me@my.mail.domain
SMTPServer=mail.my.domain.com
Signature=default
POPPassword=guessme

[ToolBar-Summary]
Bars=7
ScreenCX=800
ScreenCY=600

and the following script to retrieve mail server access information:

<?php

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig("mail.ini", "IniFile");

// get reference to [Settings] section of file
$settingsSection =& $root->getItem("section", "Settings");

// get POP account
$popAccountDirective =& $settingsSection->getItem("directive", "POPAccount");
$popAccount = $popAccountDirective->getContent();

// get password
$passwordDirective =& $settingsSection->getItem("directive", "POPPassword");
$password = $passwordDirective->getContent();

// print current configuration
echo "System is currently configured for POP account $popAccount and password $password";

?>

This follows much the same logic as before - the getItem() method is used to first obtain a reference to the [Settings] section of the INI file, and then to the appropriate directives within that section. Once the variable values have been retrieved via the getContent() method, they are printed to the output device.

Here's the output:

System is currently configured for POP account me@my.mail.domain and password guessme

When retrieving configuration data in this manner, a number of utility methods are also available; these can come in handy in certain situations. Here's a quick list:

getName() - retrieves the item name

getType() - retrieves the item type

getParent() - retrieves a reference to the item's parent

getChild() - returns a reference to the item's child

getItemPosition() - returns the item's position among its siblings

countChildren() - returns a count of the item's children

Changing Things Around

Just as you can getContent(), you can also setContent() - and this next example demonstrates how, by using this method to change a configuration variable read from a file.

<?php

// include class
include("Config.php");

// instantiate object
$c = new Config();

// read configuration data and get reference to root
$root =& $c->parseConfig("mail.ini", "IniFile");

// get reference to [Settings] section of file
$settingsSection =& $root->getItem("section", "Settings");

// get POP account
$popAccountDirective =& $settingsSection->getItem("directive", "POPAccount");
$popAccountDirective->setContent("my.new.email.address");

// write configuration back to file
$c->writeConfig();

?>

In this case, the setContent() method is used to change the value of a configuration variable, and then the writeConfig() method is used to write the entire set of variables back to the file.

This is one of the more common applications of the Config class - displaying the current configuration to the user, and saving modifications back - so pay attention to this next example, which builds on what you've just learned to create a script accepting user input for application configuration. This script is divided into two parts: a form which displays the current configuration (if available) and allows the user to edit it, and a form processor, which accepts the new configuration and saves it to a file.

<html>
<head>
</head>

<body>

<?php

if (!file_exists("ppp.conf")) {
    echo "Configuration not found!";
} else {

    // include class
    include("Config.php");

    // instantiate object
    $c = new Config();

    // read config file
    // get root
    $root =& $c->parseConfig("ppp.conf", "IniFile");

    // get sections
    $pppSection =& $root->getItem("section", "ppp");
    $userSection =& $root->getItem("section", "account");

    // get directives
    $numberDirective =& $pppSection->getItem("directive", "number");
    $retriesDirective =& $pppSection->getItem("directive", "retries");
    $deviceDirective =& $pppSection->getItem("directive", "device");
    $connect_scriptDirective =& $pppSection->getItem("directive", "connect_script");
    $ppp_userDirective =& $userSection->getItem("directive", "ppp_user");
    $ppp_passDirective =& $userSection->getItem("directive", "ppp_pass");

    // form not yet submitted
    if (!$_POST['submit']) {
        // prefill form with values from file ?>
<h2>Configuration</h2>
<table border="0" cellspacing="5" cellpadding="5">
<form action="<?=$_SERVER['PHP_SELF']?>" method="post">

<tr>
<td>ISP user name</td>
<td><input type="text" name="user" value="<?=$ppp_userDirective->getContent(); ?>"></td>
</tr>

<tr>
<td>ISP user password</td>
<td><input type="text" name="pass" value="<?=$ppp_passDirective->getContent(); ?>"></td>
</tr>

<tr>
<td>ISP dial-up number</td>
<td><input type="text" name="number" value="<?=$numberDirective->getContent(); ?>"></td>
</tr>

<tr>
<td>Number of retries</td>
<td><input type="text" name="retries" value="<?=$retriesDirective->getContent(); ?>" size="4"></td>
</tr>

<tr>
<td>Connection device</td>
<td><input type="text" name="device" value="<?=$deviceDirective->getContent(); ?>"></td>
</tr>

<tr>
<td>Connection script</td>
<td><input type="text" name="script" value="<?=$connect_scriptDirective->getContent(); ?>"></td>
</tr>

<tr>
<td colspan="2" align="center"><input type="submit" name="submit" value="Save Configuration"></td>
</tr>

</form>
</table>
<?php
    } else {
        // form submitted

        // set new values from form input
        $numberDirective->setContent($_POST['number']);
        $retriesDirective->setContent($_POST['retries']);
        $deviceDirective->setContent($_POST['device']);
        $connect_scriptDirective->setContent($_POST['script']);
        $ppp_userDirective->setContent($_POST['user']);
        $ppp_passDirective->setContent($_POST['pass']);

        // write configuration back
        $c->writeConfig();

        // print message
        echo "Configuration saved!";
    }
}
?>

</body>
</html>

Here's what it looks like:

Giving Birth

The sharp-eyed amongst you would have noticed that the previous example made one important assumption: that the configuration file existed in the first place. If it didn't, the code on the previous page would return an error - not something you want happening the first time your application is installed.

With this in mind, the Config class also comes with a number of methods that allow you to create a complete configuration file from scratch. Consider the following example, which demonstrates:

<?php

// include file
include("Config.php");

// initialize object
$c = new Config();

// create section
$mailSection =& new Config_Container("section", "mail");

// create variables/values
$mailSection->createDirective("name", "Bruce Banner");
$mailSection->createDirective("email", "hulk@angry.green.guy");
$mailSection->createDirective("host", "mail.apollo.domain");

// reassign root
$c->setRoot($mailSection);

// write configuration to file
$c->writeConfig("mail.conf", "INIFile");

?>

Here's the output:

[mail]
name=Bruce Banner
email=hulk@angry.green.guy
host=mail.apollo.domain

In this case, an object of the Config_Container class is initialized as a section named "mail", and a reference is obtained to it. This reference is used to build the rest of the configuration tree via the createDirective() methods. Once the tree is complete, a call to setRoot() takes care of resetting the root reference to the newly-created section, and a call to writeConfig() writes the entire tree to the named file.

In addition to the createDirective() method, the class also includes createSection(), createComment() and createBlank() methods, designed specifically to create sections, comments and blank lines respectively. Take a look at the manual pages for more on these methods, together with usage examples.

Link Zone

That's about it for the moment. In this article, I introduced you to the Config class, which is designed primarily to assist you in the reading, writing and manipulation of configuration data. I showed you how to read and write configuration files in XML, PHP and INI formats, how to create configuration files from scratch, and how to use Config's built-in methods to simplify and speed up Web-based configuration tool development.

Of course, whatever you've seen over the past few pages is just the top of the iceberg. Config comes with a large number of different methods, allowing you to do all kinds of interesting things like creating configuration sections and variables from scratch, dynamically adding and removing variables from an existing configuration, shifting the positions of sections and variables around within a file, and much more.

If you'd like to learn more about Config and its related tools, you should take a look at the following links:

The Config package on PEAR, at http://pear.php.net/Config

Config API documentation, at http://pear.php.net/manual/en/package.configuration.config.intro.php

The official PEAR Web site, at http://pear.php.net/

Till next time...ciao!

Note: Examples are illustrative only, and are not meant for a production environment. Examples in this tutorial have been tested on Linux/i586 with Apache 1.3.26, PHP 5.0 and Config 1.9. Melonfire provides no warranties or support for the source code described in this article. YMMV!

This article was first published on22 Aug 2003.