<?
/* ================================================================================================= *\
# + ---------------------------------+ #
# | Guestbook class | #
# | 1001x2+2 © jShadow | #
# | HCM City, Vietnam | #
# | Contact: | #
# | Phone: +84903913540 | #
# | Email: hide@address.com | #
# | hide@address.com | #
# | hide@address.com | #
# +----------------------------------+ #
# . Thanks for trying and using this class. #
# . I appreciate you helping me find out the bugs. #
# Any ideas and bug reports, please contact me. #
# #
# #
# +--------------+ #
# | Introduction | #
# +--------------+ #
# With this class, you can write a popular Guestbook application easily and flexibly. You can choose #
# which type of database will be used, either XML database (known as flat db) or real database. Now, #
# this class supports MySQL only, but I'm woking on it so that our class can work with PostgreSQL, #
# MSSQL, Oracle, etc... in future. #
# #
# +----------+ #
# | Features | #
# +----------+ #
# . Get and sort records #
# . Add new records #
# . Delete records #
# . Update a record #
# #
# +--------------+ #
# | Requirements | #
# +--------------+ #
# . PHP 4.3.0 or later #
# . XML lib (for flat DB) #
# . MySQL 3.23.49 or later (for MySQL database) #
# . This class probably works with the following configuration: #
# + magic_quotes_runtime = Off #
# #
# +----------------+ #
# | Public methods | #
# +----------------+ #
# 0. Guestbook($field_array) // CONSTRUCTOR #
# This CONSTRUCTOR will create a fields order for SQL DB or FLAT DB. #
# All of data put into DB must be arranged in this order. #
# If you use SQL DB, besides essential fields of a guestbook, you should #
# provide a field in order that we can handle records' ids and put it as #
# the first field. For ex: #
# + You choose FLAT DB: #
# $field_array = array('author', 'email', ...) #
# It will create field "id" automatically to handle our #
# guestbook's records #
# + You choose SQL DB: #
# $field_array = array('id', 'author', 'email', ...) #
# My field "id" will be set like this one: #
# `id` INT( 3 ) UNSIGNED NOT NULL #
# #
# 1. gbConfig($db_info) #
# This function is used to switch between SQL DB and FLAT DB. #
# It will create XML file for Flat DB or get SQL DB info. #
# + If using Flat DB, you provide a file name (including path is optional). #
# + If using SQL DB: #
# - First, create a table for this Guestbook manually. Table's fields #
# must be arranged in order of the CONSTRUCTOR. #
# - Second, provide an array. This array's values must be arranged in following #
# order: #
# $db_info = array('host', 'user', 'pass', 'dbname', 'table') #
# #
# 2. putIntoDB($data_array) #
# This function is used to put data into FLAT DB or SQL DB. #
# Values of array $data_array must be arranged in order of the CONSTRUCTOR. #
# #
# 3. getRecords($sort_field = "id", $sort_type = "ASC", $from = 0, $limit = 0) #
# This function is used to get data from FLAT DB or SQL DB. It returns an array and each #
# element of this array is a record. Each record is an array whose keys are guestbook's #
# fields with correlative values. #
# - $sort_field is used to sort. #
# - $sort_type has either ASC (ascending) or DESC (descending) value. #
# - $from is the record's ordinal number in FLAT DB or SQL DB. #
# - $limit is the quantity of records you want to get. #
# - With default values, this function will get all records from db and these records #
# will be sorted by id's field and arranged in acsending order. #
# #
# 4. getToEdit($id) #
# This function is used to get a record determined by $id so that you can edit later. #
# #
# 5. updateRecord($id, $data_array) #
# This function is used to update an edited record. #
# Values of array $data_array must be arranged in order of CONSTRUCTOR. #
# #
# 6. deleteRecord($id) #
# This function is used to delete one record only. #
# #
# 7. getMaxID() #
# This function is used to get the max ID of records. #
# #
# 8. getRecordsTotal() #
# This function is used to get the total number of records. #
# #
# #
# ------------------------- #
# . Thanks to all of my friends and Open Source community. #
# . A very special thanks to my mother, Ho Thi Le, and my Hanna, Pham Thi Thu Ha, #
# for all of their love, understanding and inspiration. #
# ------------------------- #
# jShadow #
\* ================================================================================================= */
# ---===--------------===--- #
# ---=== PUBLIC CLASS ===--- #
# ---===--------------===--- #
class Guestbook
{
var $gb_fields;
var $db_using;
var $db_driver;
var $sql_table;
var $sort_field;
var $sort_type;
// constructor
function Guestbook($field_array) { // ex: $field_array = $array('author', 'email', 'url', 'comment', 'date', ...)
$this->gb_fields = $field_array;
$this->db_using = NULL;
$this->db_driver = NULL;
$this->sql_table = NULL;
$this->sort_field = NULL;
$this->sort_type = NULL;
}
## --------------------- ##
## -----+++ Pri +++----- ##
## --------------------- ##
function getAllRecords() {
switch ($this->db_using) {
case "FLAT" :
$records_array = $this->db_driver->parseXMLEntry();
return $records_array;
break;
case "SQL" :
break;
}
}
// this callback function is used to sort
function _cmp($array_a, $array_b) {
$array_a[$this->sort_field] = strtolower($array_a[$this->sort_field]);
$array_b[$this->sort_field] = strtolower($array_b[$this->sort_field]);
if ($array_a[$this->sort_field] == $array_b[$this->sort_field])
return 0;
if ($this->sort_type == "DESC") {
return ( $array_a[$this->sort_field] < $array_b[$this->sort_field] ) ? 1 : -1;
}
else
return ( $array_a[$this->sort_field] < $array_b[$this->sort_field] ) ? -1 : 1;
}
// each value of $array is an array
function makeSafeOutput(&$array) {
switch ($this->db_using) {
case "FLAT":
foreach (array_keys($array) as $k) {
ValidIO::makeSafeOutputFLAT($array[$k]);
}
break;
case "SQL":
foreach (array_keys($array) as $k) {
ValidIO::makeSafeOutputSQL($array[$k]);
}
break;
}
}
## --------------------- ##
## -----+++ Pub +++----- ##
## --------------------- ##
function gbConfig($db_info) {
if ( !is_array($db_info) ) {
$this->db_using = "FLAT";
$this->db_driver = new XMLdb($db_info, $this->gb_fields);
}
else {
$this->db_using = "SQL";
$this->sql_table = $db_info[4];
$this->db_driver = new SQL($db_info[0], $db_info[1], $db_info[2], $db_info[3]);
}
}
function putIntoDB($data_array) {
switch ($this->db_using) {
case "FLAT" :
ValidIO::makeSafeInputFLAT($data_array);
if ( !$this->db_driver->addNewEntry($data_array) )
die("Could not put data into DB now or your permission has been denied.");
break;
case "SQL" :
ValidIO::makeSafeInputSQL($data_array);
// prepare next record's ID
$id = $this->getMaxID() + 1;
// prepare data
$values = implode("', '", $data_array);
$values = "'" . $id . "', '" . $values . "'";
$sql = "INSERT INTO " . $this->sql_table . " VALUES (" . $values . ")";
$this->db_driver->query($sql);
break;
}
}
function getRecords($sort_field = "id", $sort_type = "ASC", $from = 0, $limit = 0) {
// prepare for callback function _cmp($array_a, $array_b)
$this->sort_field = ( in_array(strval($sort_field), $this->gb_fields) ) ? $sort_field : 'id';
$this->sort_type = ( strtoupper($sort_type) == "DESC" ) ? "DESC" : "ASC";
if ($from < 0) { $from = 0; }
switch ($this->db_using) {
case "FLAT" :
// sort
$all_records = $this->getAllRecords();
usort($all_records, array($this, "_cmp"));
// return the records limited by $from and $limit
$record_total = count($all_records);
$record_max = $record_total - 1;
if ($from > $record_max) { $from = $record_max; }
$to = $from + $limit;
if ($to < $from) { $to = $from + 1; }
if ($to == $from || $to > $record_total) { $to = $record_total; }
$results = array();
for ($i = $from; $i < $to; $i++) {
$results[] = $all_records[$i];
}
break;
case "SQL" :
if ($limit == 0) { $limit = $this->getRecordsTotal(); }
$sql = "SELECT * FROM " . $this->sql_table .
" ORDER BY " . $this->sort_field . " " . $this->sort_type .
" LIMIT " . $from . ", " . $limit;
$this->db_driver->query($sql);
$results = $this->db_driver->fetchArray();
break;
}
$this->makeSafeOutput($results);
return $results;
}
function deleteRecord($id) {
$id = intval($id);
switch ($this->db_using) {
case "FLAT" :
if (!$this->db_driver->deleteEntry($id))
die("Could not delete this record (<b>#" . $id . "</b>) now. Try again.");
break;
case "SQL" :
$sql = "DELETE FROM " . $this->sql_table .
" WHERE " . $this->gb_fields[0] . " = '" . $id . "'";
$this->db_driver->query($sql);
break;
}
}
function getToEdit($id) {
$id = intval($id);
switch ($this->db_using) {
case "FLAT" :
$record = $this->db_driver->getOneEntry($id);
ValidIO::makeSafeOutputFLAT($record);
break;
case "SQL" :
$sql = "SELECT * FROM " . $this->sql_table .
" WHERE " . $this->gb_fields[0] . " = '" . $id . "' LIMIT 0, 1";
$this->db_driver->query($sql);
$record_array = $this->db_driver->fetchArray();
$record = $record_array[0];
break;
}
return $record;
}
function updateRecord($id, $data_array) {
$id = intval($id);
switch ($this->db_using) {
case "FLAT" :
ValidIO::makeSafeInputFLAT($data_array);
if ( !$this->db_driver->addNewEntry($data_array, $id) )
die("Could not edit this record (<b>#" . $id . "</b>) now.");
break;
case "SQL" :
ValidIO::makeSafeInputSQL($data_array);
$set = NULL;
$c = count($data_array) - 1;
for ($i = 0; $i <= $c ; $i++) {
$j = $i + 1;
$set .= ($i != $c) ? $this->gb_fields[$j] . " = '" . $data_array[$i] . "', " : $this->gb_fields[$j] . " = '" . $data_array[$i] . "'";
}
$sql = "UPDATE " . $this->sql_table .
" SET " . $set .
" WHERE " . $this->gb_fields[0] . " = '" . $id . "'";
$this->db_driver->query($sql);
break;
}
}
function getMaxID() {
switch ($this->db_using) {
case "FLAT" :
return $this->db_driver->getMaxID();
break;
case "SQL" :
return $this->db_driver->getMaxID($this->sql_table, $this->gb_fields[0]);
break;
}
}
function getRecordsTotal() {
switch ($this->db_using) {
case "FLAT" :
return $this->db_driver->getEntryTotal();
break;
case "SQL" :
return $this->db_driver->getRowsTotal($this->sql_table);
break;
}
}
} // END CLASS Guestbook
# ---===---------------===--- #
# ---=== PRIVATE CLASS ===--- #
# ---===---------------===--- #
class XMLdb
{
var $xml_db;
var $xml_fields;
var $xml_fields_total;
var $xml_parser;
var $xml_results;
var $xml_temp;
var $cur_tag;
/* =============================================================
// This CONSTRUCTOR will create an XML file, which is considered
// as Flat DB, whose form is like that:
// <!DOCTYPE GUESTBOOK [
// <!ELEMENT GB (AUTHOR, EMAIL, DATE, COMMENT)>
// <!ATTLIST GB ID ID #REQUIRED>
// <!ELEMENT AUTHOR (#PCDATA)>
// <!ELEMENT EMAIL (#PCDATA)>
// <!ELEMENT COMMENT (#PCDATA)>
// ]>
//
// <GUESTBOOK>
// </GUESTBOOK>
=============================================================== */
function XMLdb($filename, $field_array) {
$this->xml_db = $filename;
$this->xml_fields = array();
$this->xml_results = array();
$this->xml_temp = array();
$this->cur_tag = NULL;
foreach ($field_array as $v) {
$this->xml_fields[] = strtoupper($v);
}
$this->xml_fields_total = count($this->xml_fields);
if ( !file_exists($this->xml_db) ) {
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\r\n";
$xml .= "<!DOCTYPE GUESTBOOK [\r\n";
$xml .= "\t<!ELEMENT GB (";
$xml_GB_tags = NULL;
$xml_elements = NULL;
$c = $this->xml_fields_total - 1;
for ($i = 0; $i <= $c; $i++) {
$xml_GB_tags .= $this->xml_fields[$i];
$xml_GB_tags .= ( $i != $c ) ? ", " : ")>\r\n";
$xml_elements .= "\t<!ELEMENT " . $this->xml_fields[$i] . " (#PCDATA)>\r\n";
}
$xml .= $xml_GB_tags;
$xml .= "\t\t<!ATTLIST GB ID ID #REQUIRED>\r\n";
$xml .= $xml_elements;
$xml .= "]>\r\n";
$xml .= "<GUESTBOOK>\r\n";
$xml .= "</GUESTBOOK>";
if ( !$this->makeXMLdatabase($xml) ) {
echo "<p><ul type=\"disc\"><li>Could not make XML Structure for FLAT DB.</li>";
echo "<li>You can create an XML file manually, copy and paste the structure below.</li>";
echo "<li>Remember:";
echo "<ul type=\"square\"><li>Change this XML file's mode to 666 or 777.";
echo "<li>DO NOT leave any space or blank line at the end.</ul></ul></p>";
echo "<hr size=\"1\" color=\"red\"><br>";
$this->dumpStructure($xml);
die("<hr size=\"1\" color=\"red\"><br>");
}
}
}
## --------------------- ##
## -----+++ Pri +++----- ##
## --------------------- ##
function makeXMLdatabase($xml) {
ignore_user_abort(true);
$fp = fopen($this->xml_db, "x+b");
if (!$fp)
return false;
else {
if (flock($fp, 2)) {
fwrite($fp, $xml);
fflush($fp);
flock($fp, 3);
}
else
return false;
fclose($fp);
}
ignore_user_abort(false);
return true;
}
function dumpStructure($content) {
echo nl2br(preg_replace("/\t/", " ", htmlspecialchars($content)));
}
function makeXMLEntry($id, $data_array) {
$xml = "\t<GB ID=\"" . $id . "\">\r\n";
for ($i = 0; $i < $this->xml_fields_total; $i++) {
if ( !empty($data_array[$i]) )
$xml .= "\t\t<" . $this->xml_fields[$i] . ">" . $data_array[$i] . "</" . $this->xml_fields[$i] . ">\r\n";
}
$xml .= "\t</GB>\r\n";
return $xml;
}
function streamWriteNewEntry($fp, $xml) {
fseek($fp, -12, SEEK_END);
fwrite($fp, $xml);
fwrite($fp, "</GUESTBOOK>");
fflush($fp);
}
function streamDeleteEntry($fp, $id) {
fseek($fp, 0, SEEK_END);
$filesize = ftell($fp);
rewind($fp);
$text = preg_replace("/\t<GB ID=\"" . $id . "\">.*?<\/GB>\r\n/is", NULL, fread($fp, $filesize), 1);
rewind($fp);
fwrite($fp, $text);
fflush($fp);
ftruncate($fp, ftell($fp));
}
function streamGetIDTags($fp) {
fseek($fp, 0, SEEK_END);
$filesize = ftell($fp);
rewind($fp);
$text = fread($fp, $filesize);
preg_match_all("/<GB ID=\"\d+\">/si", $text, $re);
return $re[0];
}
function streamGetMaxID($fp) {
$id_tags_array = $this->streamGetIDTags($fp);
foreach ($id_tags_array as $k => $v) {
$id_tags_array[$k] = preg_replace("/[^\d]/", NULL, $v);
}
sort($id_tags_array);
return end($id_tags_array);
}
function startElementHandler($xml, $element, $attribute) {
$this->cur_tag = $element;
if ( strcmp($element, "GB") == 0 ) {
$this->xml_temp['id'] = $attribute['ID'];
}
}
function cdataHandler($xml, $data) {
$data = eregi_replace(">" . "[[:space:]]+" . "<", "><", $data);
if ( in_array($this->cur_tag, $this->xml_fields) ) {
$this->xml_temp[strtolower($this->cur_tag)] .= $data;
}
}
function endElementHandler($xml, $element) {
if ( strcmp($element, "GB") == 0 ) {
$this->xml_results[] = $this->xml_temp;
$this->xml_temp = array();
$this->cur_tag = NULL;
}
}
## --------------------- ##
## -----+++ Pub +++----- ##
## --------------------- ##
// Values of $data_array must be arranged in order of $this->xml_fields
// If $id is NULL, this function will increase id's value automatically
// If $id is other than NULL, this function will delete the entry whose
// id is equal to this id's value and add a new entry with this id.
function addNewEntry($data_array, $id = NULL) {
ignore_user_abort(true);
$fp = fopen($this->xml_db, "r+b");
if (!$fp)
return false;
else {
if (flock($fp, 2)) {
if ($id == NULL) {
// get the last record's ID
$last_id = $this->streamGetMaxID($fp);
$id = $last_id + 1;
}
else {
$this->streamDeleteEntry($fp, $id);
}
// prepare data to put into flat db
$xml = $this->makeXMLEntry($id, $data_array);
// write new record
$this->streamWriteNewEntry($fp, $xml);
flock($fp, 3);
}
else
return false;
fclose($fp);
}
ignore_user_abort(false);
return true;
}
function parseXMLEntry() {
$this->xml_parser = xml_parser_create("UTF-8");
xml_set_object($this->xml_parser, $this);
xml_set_element_handler($this->xml_parser, "startElementHandler", "endElementHandler");
xml_set_character_data_handler($this->xml_parser, "cdataHandler");
xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, 1);
$fp = fopen($this->xml_db, "rb");
if (!$fp)
return false;
else {
while ($data = fread($fp, filesize($this->xml_db))) {
xml_parse($this->xml_parser, $data, feof($fp)) or die("<b>FLAT DB error at line " . xml_get_current_line_number($this->xml_parser) . "</b>");
}
xml_parser_free($this->xml_parser);
fclose($fp);
return $this->xml_results;
}
}
function deleteEntry($id) {
ignore_user_abort(true);
$fp = fopen($this->xml_db, "r+b");
if (!$fp)
return false;
else {
if (flock($fp, 2)) {
$this->streamDeleteEntry($fp, $id);
flock($fp, 3);
}
else
return false;
fclose($fp);
}
ignore_user_abort(false);
return true;
}
function getOneEntry($id) {
$fp = @fopen($this->xml_db, "rb");
if (!$fp)
return false;
else {
while (!feof($fp)) {
if ( preg_match("/<GB ID=\"" . $id . "\">/i", fgets($fp, 4096), $re) ) {
$text = NULL;
while ( !preg_match("/<\/GB>/i", $buffer = fgets($fp, 4096)) ) {
$text .= $buffer;
}
break;
}
}
fclose($fp);
$re = array();
preg_match_all("/(<([\S]+)[^>]*>)(.*)(<\/\\2>)/is", $text, $re);
$record = array();
$record['id'] = $id;
for ($i = 0; $i < $this->xml_fields_total; $i++) {
$record[strtolower($re[2][$i])] = $re[3][$i];
}
}
return $record;
}
function getMaxID() {
$fp = fopen($this->xml_db, "rb");
if (!fp)
return false;
else {
if (flock($fp, 2)) {
$max_id = $this->streamGetMaxID($fp);
flock($fp, 3);
}
else
return false;
fclose($fp);
}
return $max_id;
}
function getEntryTotal() {
$fp = fopen($this->xml_db, "rb");
if (!fp)
return false;
else {
if (flock($fp, 2)) {
$id_tags = $this->streamGetIDTags($fp);
flock($fp, 3);
}
else
return false;
fclose($fp);
}
return count($id_tags);
}
} // END CLASS XMLdb
# ---===---------------===--- #
# ---=== PRIVATE CLASS ===--- #
# ---===---------------===--- #
class SQL
{
var $result;
function SQL($host, $user, $pass, $db) {
@mysql_connect($host, $user, $pass) or die("Could not connect to host.");
@mysql_select_db($db) or die("Could not select db.");
}
function query($sql) {
$this->result = @mysql_query($sql) or die("Could not query.");
if (strtolower(gettype($this->result)) == "resource") { return $this->result; }
}
function numRows() {
return @mysql_num_rows($this->result);
}
function fetchArray() {
while ($row = @mysql_fetch_array($this->result))
$re[] = $row;
return $re;
}
function getRowsTotal($table) {
$sql = "SELECT * FROM " . $table;
$re = mysql_num_rows($this->query($sql));
if ($re)
return $re;
else
return 0;
}
function getMaxID($table, $id_field) {
$sql = "SELECT MAX(" . $id_field . ") FROM " . $table;
$re = mysql_fetch_row($this->query($sql));
return $re[0];
}
} // END CLASS SQL
# ---===---------------===--- #
# ---=== PRIVATE CLASS ===--- #
# ---===---------------===--- #
class ValidIO
{
function makeSafeInputFLAT(&$input) {
$pattern = array("/&/", "/</", "/>/");
$replace = array("&", "<", ">");
if ( get_magic_quotes_gpc() ) {
if ( is_array($input) ) {
foreach ($input as $k => $v) {
$input[$k] = preg_replace($pattern, $replace, trim($v));
}
}
else {
$input = preg_replace($pattern, $replace, trim($input));
}
}
else {
if ( is_array($input) ) {
foreach ($input as $k => $v) {
$input[$k] = addslashes(preg_replace($pattern, $replace, trim($v)));
}
}
else {
$input = addslashes(preg_replace($pattern, $replace, trim($input)));
}
}
}
function makeSafeInputSQL(&$input) {
if ( get_magic_quotes_gpc() ) {
if ( is_array($input) ) {
foreach ($input as $k => $v) {
$input[$k] = trim($v);
}
}
else {
$input = trim($input);
}
}
else {
if ( is_array($input) ) {
foreach ($input as $k => $v) {
$input[$k] = addslashes(trim($v));
}
}
else {
$input = addslashes(trim($input));
}
}
}
function makeSafeOutputFLAT(&$output) {
$pattern = array("/</", "/>/", "/&/");
$replace = array("<", ">", "&");
if ( is_array($output) ) {
foreach ($output as $k => $v) {
$output[$k] = stripslashes(htmlspecialchars(preg_replace($pattern, $replace, $v), ENT_QUOTES));
}
}
else {
$output = stripslashes(htmlspecialchars(preg_replace($pattern, $replace, $output), ENT_QUOTES));
}
}
function makeSafeOutputSQL(&$output) {
if ( is_array($output) ) {
foreach ($output as $k => $v) {
$output[$k] = htmlspecialchars($v, ENT_QUOTES);
}
}
else {
$output = htmlspecialchars($output, ENT_QUOTES);
}
}
}//END CLASS ValidInput
?>