<?php
/**
* "Takes an XML file, parses it and dynamically creates specific wrapper functions that connect
* to a database to read/write data."
* @author: Claudius Tiberiu Iacob <hide@address.com>
* @license: Creative Commons Attribution Share Alike - Claudius Tiberiu Iacob 2009
*/
class DbProxy extends ParamsProxy {
const FAILURE = "FAILURE";
const SUCCESS = "SUCCESS";
const NOT_CONFIGURED = "NOT_CONFIGURED";
const CANNOT_REDEFINE_STUB = "CANNOT_REDEFINE_STUB";
const ILLEGAL_STUB_NAME = "ILLEGAL_STUB_NAME";
const ILLEGAL_STUB_ARGUMENTS = "ILLEGAL_STUB_ARGUMENTS";
const MISSING_STUB_QUERY = "MISSING_STUB_QUERY";
const ILLEGAL_STUB_QUERY = "ILLEGAL_STUB_QUERY";
const ILLEGAL_STUB_OUTPUT_TYPE = "ILLEGAL_STUB_OUTPUT_TYPE";
const UNMATCHED_STUB_ARGUMENTS = "UNMATCHED_STUB_ARGUMENTS";
const SQL_QUERY_FAILED = "SQL_QUERY_FAILED";
const CLASS_NOT_REGISTERED = "CLASS_NOT_REGISTERED";
const INVALID_CONNECTION_SETTINGS = "INVALID_CONNECTION_SETTINGS";
const INVALID_DB_NAME = "INVALID_DB_NAME";
const INVALID_HOST_NAME = "INVALID_HOST_NAME";
const INVALID_USER_NAME = "INVALID_USER_NAME";
const INVALID_USER_PASSWORD = "INVALID_USER_PASSWORD";
const ROOT_LOGIN_IS_INSECURE = "ROOT_LOGIN_IS_INSECURE";
const CONNECTION_FAILED = "CONNECTION_FAILED";
const NOT_CALLABLE_STUB = "NOT_CALLABLE_STUB";
const NO_STUBS = "NO_STUBS";
const NO_CONNECTION_SETTINGS = "NO_CONNECTION_SETTINGS";
const ARGUMENTS_MISSMATCH = "ARGUMENTS_MISSMATCH";
private $isInitialized = false;
private $stubs = null;
private $resourceLinks = null;
private $currClientClsName = null;
private $clientConfigFileName = "dbAccess.xml";
private $connectionStubName = "connection";
private $paramsKeyName = "queryParams";
private $templateKeyName = "queryTemplate";
private $outputTypeKeyName = "queryResultType";
private $paramsListDelimiter = ",";
private $outputType_string = "string";
private $outputType_number = "number";
private $outputType_boolean = "boolean";
private $outputType_array = "array";
private $outputType_null = "null";
private $dbKeyName = "dbName";
private $hostKeyName = "dbHost";
private $userKeyName = "dbUser";
private $passKeyName = "dbPassword";
private $mysqlForbiddenChars = null;
private $safePass = null;
private $fqdnHost = null;
private $encIdentifierNamePatt = null;
private $identifierMaxChars = 64;
private $digitToLettersMap = null;
public function __construct () {
// Try to make sure we work on UTF-8 encoded strings:
if (!headers_sent ()) {
header('Content-type: text/html; charset=utf-8', true);
}
mb_language('uni');
mb_internal_encoding('UTF-8');
parent::__construct();
$this->stubs = array ();
$this->resourceLinks = array();
$this->mysqlForbiddenChars = array (0, 10, 13, 92, 39, 34, 26, 45, 96);
$this->safePass = $this->safePassPattern();
$this->fqdnHost = $this->hostPattern();
$this->encIdentifierNamePatt = $this->makeEncIdentifierPattern ();
$this->digitToLettersMap = array (48 => "A", 49 => "B", 50 => "C", 51 => "D", 52 => "F", 53 => "G",
54 => "H", 55 => "I", 56 => "J", 57 => "K");
}
protected function getConfigFileName () {
return $this->clientConfigFileName;
}
protected function applyParams ($targetInstance, $params) {
parent::applyParams($this, $params);
}
public function configure ($instance) {
$this->currClientClsName = get_class ($instance);
parent::configure($instance);
$this->proofConfiguration ();
$this->isInitialized = true;
}
private function proofConfiguration () {
if (count ($this->stubs) == 0) {
trigger_error(DbProxy::NO_STUBS, E_USER_ERROR);
}
$hasCSets = false;
$hasStubs = false;
foreach ($this->stubs as $key => $value) {
if (preg_match('/__'.$this->connectionStubName.'$/', $key) === 1) {
$hasCSets = true;
} else {
$hasStubs = true;
}
}
if (!$hasCSets) {
trigger_error(DbProxy::NO_CONNECTION_SETTINGS, E_USER_ERROR);
}
if (!hasStubs) {
trigger_error(DbProxy::NO_STUBS, E_USER_ERROR);
}
}
private function __call ($funcName, $arguments) {
$fname = preg_replace('/^set/', "", $funcName);
$trace = debug_backtrace();
$callingClsName = $trace[2]["class"];
$parentClassName = $this->getParentClassName();
if ($callingClsName === $parentClassName) {
// building stubs
$fname = trim ($fname);
if ($fname === "") {
trigger_error(DbProxy::ILLEGAL_STUB_NAME);
}
if (strpos($fname, " ") !== false) {
trigger_error(DbProxy::ILLEGAL_STUB_NAME);
}
if (strpos($fname, "__") === 0) {
trigger_error(DbProxy::ILLEGAL_STUB_NAME);
}
if (preg_match('/^[_a-zA-Z]?[_a-zA-Z0-9]*$/', $fname) === 0) {
trigger_error(DbProxy::ILLEGAL_STUB_NAME);
}
$fname = strtolower($fname[0]) . substr($fname, 1);
if (array_key_exists($fname, $this->stubs)) {
trigger_error(DbProxy::CANNOT_REDEFINE_STUB, E_USER_ERROR);
}
if (strcasecmp($fname, $this->connectionStubName) === 0) {
// setup custom connection for this class
$cnData = $arguments[0];
if (!is_array($cnData)) {
trigger_error(DbProxy::INVALID_CONNECTION_SETTINGS, E_USER_ERROR);
}
if (!array_key_exists($this->dbKeyName, $cnData)) {
trigger_error(DbProxy::INVALID_CONNECTION_SETTINGS, E_USER_ERROR);
}
if (!array_key_exists($this->hostKeyName, $cnData)) {
trigger_error(DbProxy::INVALID_CONNECTION_SETTINGS, E_USER_ERROR);
}
if (!array_key_exists($this->userKeyName, $cnData)) {
trigger_error(DbProxy::INVALID_CONNECTION_SETTINGS, E_USER_ERROR);
}
if (!array_key_exists($this->passKeyName, $cnData)) {
trigger_error(DbProxy::INVALID_CONNECTION_SETTINGS, E_USER_ERROR);
}
$cDbName = $cnData[$this->dbKeyName];
$cDbName = trim ($cDbName);
if ($cDbName === "") {
trigger_error(DbProxy::INVALID_DB_NAME, E_USER_ERROR);
}
$cDbHost = $cnData[$this->hostKeyName];
if (strpos($cDbHost, "http://") === 0) {
$cDbHost = substr($cDbHost, 7);
} elseif (strpos($cDbHost, "https://") === 0) {
$cDbHost = substr($cDbHost, 8);
}
$cDbHost = trim ($cDbHost);
if ($cDbHost === "") {
trigger_error(DbProxy::INVALID_HOST_NAME, E_USER_ERROR);
}
if (strlen($cDbHost) > 255) {
trigger_error(DbProxy::INVALID_HOST_NAME, E_USER_ERROR);
}
$cDbHost = strtolower($cDbHost);
if (preg_match ($this->fqdnHost, $cDbHost) === 0) {
trigger_error(DbProxy::INVALID_HOST_NAME, E_USER_ERROR);
}
$cDbUser = $cnData[$this->userKeyName];
$cDbUser = trim ($cDbUser);
if ($cDbUser === "") {
trigger_error(DbProxy::INVALID_USER_NAME, E_USER_ERROR);
}
if (preg_match('/[a-zA-Z0-9_]+/', $cDbUser) === 0) {
trigger_error(DbProxy::INVALID_USER_NAME, E_USER_ERROR);
}
if ($cDbUser === "root") {
trigger_error(DbProxy::ROOT_LOGIN_IS_INSECURE, E_USER_ERROR);
}
$cDBPass = $cnData[$this->passKeyName];
$cDBPass = trim ($cDBPass);
if ($cDBPass === "") {
trigger_error(DbProxy::INVALID_USER_PASSWORD);
}
if (preg_match($this->safePass, $cDBPass) === 0) {
trigger_error(DbProxy::INVALID_USER_PASSWORD);
}
$link = @mysql_connect($cDbHost, $cDbUser, $cDBPass);
if ($link === false) {
trigger_error(DbProxy::CONNECTION_FAILED . "\n" . mysql_errno() . ": " .
$this->makeReadable(mysql_error()), E_USER_ERROR);
}
$res = mysql_query("USE " . $cDbName);
if ($res === false) {
trigger_error(DbProxy::INVALID_DB_NAME . "\n" . mysql_errno() . ": " .
$this->makeReadable (mysql_error()), E_USER_ERROR);
}
$this->resourceLinks [$this->currClientClsName] = $link;
$this->stubs[$this->currClientClsName . "__" . $this->connectionStubName] = array (
$cDbUser, $cDBPass, $cDbHost, $cDbName);
} else {
// setup operations for this class
$fdata = $arguments[0];
if (!is_array($fdata)) {
trigger_error(DbProxy::ILLEGAL_STUB_ARGUMENTS, E_USER_ERROR);
}
$fArgs = "";
$fArgsList = array();
if (array_key_exists($this->paramsKeyName, $fdata)) {
$fArgs = $fdata[$this->paramsKeyName];
$fArgs = trim ($fArgs);
if ($fArgs !== "") {
$fArgs = explode($this->paramsListDelimiter, $fArgs);
for ($i=0; $i<count($fArgs); $i++) {
$fArg = $fArgs[$i];
$fArg = trim ($fArg);
if ($fArg === "") {
trigger_error(DbProxy::ILLEGAL_STUB_ARGUMENTS, E_USER_ERROR);
}
if (strpos($fArg, " ") !== false) {
trigger_error(DbProxy::ILLEGAL_STUB_ARGUMENTS, E_USER_ERROR);
}
if (strpos($fArg, "__") === 0) {
trigger_error(DbProxy::ILLEGAL_STUB_ARGUMENTS, E_USER_ERROR);
}
if (preg_match('/^[_a-zA-Z]?[_a-zA-Z0-9]*$/', $fArg) === 0) {
trigger_error(DbProxy::ILLEGAL_STUB_ARGUMENTS, E_USER_ERROR);
}
array_push($fArgsList, $fArg);
}
}
if (!array_key_exists($this->templateKeyName, $fdata)) {
trigger_error(DbProxy::MISSING_STUB_QUERY, E_USER_ERROR);
}
}
$fTempl = $fdata[$this->templateKeyName];
$fTempl = trim ($fTempl);
$fTempl = preg_replace('/[\n\r\t]/', " ", $fTempl);
$fTempl = preg_replace('/\s{2,}/', " ", $fTempl);
if ($fTempl === "") {
trigger_error(DbProxy::ILLEGAL_STUB_QUERY, E_USER_ERROR);
}
$fOutType = $this->outputType_array;
if (array_key_exists($this->outputTypeKeyName, $fdata)) {
$fOutType = $fdata[$this->outputTypeKeyName];
$fOutType = trim ($fOutType);
if ($fOutType === "") {
trigger_error(DbProxy::ILLEGAL_STUB_OUTPUT_TYPE, E_USER_ERROR);
}
$fOutType = strtolower ($fOutType);
if ($fOutType !== $this->outputType_string &&
$fOutType !== $this->outputType_number &&
$fOutType !== $this->outputType_boolean &&
$fOutType !== $this->outputType_array &&
$fOutType !== $this->outputType_null) {
trigger_error(DbProxy::ILLEGAL_STUB_OUTPUT_TYPE, E_USER_ERROR);
}
}
$this->stubs[$this->currClientClsName . "__" . $fname] = array ($fArgsList, $fTempl,
$fOutType);
}
} else {
// merely use a stub
if (!$this->isInitialized) {
trigger_error(DbProxy::NOT_CONFIGURED, E_USER_ERROR);
}
$stubName = $this->currClientClsName . "__" . $fname;
if ($stubName === $this->currClientClsName . "__" . $this->connectionStubName) {
trigger_error(DbProxy::NOT_CALLABLE_STUB, E_USER_ERROR);
}
if (!array_key_exists($stubName, $this->stubs)) {
trigger_error (DbProxy::CLASS_NOT_REGISTERED, E_USER_ERROR);
}
return $this->runStub($arguments, $this->stubs[$stubName]);
}
}
private function runStub ($arguments, $stub) {
$argList = $stub[0];
if (count($argList) !== count ($arguments)) {
trigger_error(DbProxy::ARGUMENTS_MISSMATCH, E_USER_ERROR);
}
$template = $stub[1];
$outputType = $stub[2];
for ($i=0; $i<count($arguments); $i++) {
$argVal = $arguments[$i];
$argName = $argList[$i];
$template = $this->fillInQueryTemplate($template, $argName, $argVal);
}
$this->proofQuery($template);
$result = @$this->executeQuery($template);
$ret = $this->fetchQueryResult($result, $outputType);
if ($outputType !== $this->outputType_null) {
return $ret;
}
}
private function fillInQueryTemplate ($template, $paramName, $paramValue) {
$paramValue = $this->unicodeEncode ($paramValue);
$template = str_replace('%' . $paramName . '%', $paramValue, $template);
return $template;
}
private function proofQuery ($query) {
if (preg_match('/\x25[^\x25]+\x25/', $query) !== 0) {
trigger_error(DbProxy::UNMATCHED_STUB_ARGUMENTS, E_USER_ERROR);
}
}
private function getClientResource ($clientClsName) {
return $this->resourceLinks[$clientClsName];
}
private function executeQuery ($query) {
return mysql_query ($query, $this->getClientResource($this->currClientClsName));
}
private function fetchQueryResult ($result, $type) {
$fetchedResult = array ();
$numRows = 0;
$affectedRows = 0;
if (!is_resource($result)) {
if ($result === false) {
$err = mysql_error($this->getClientResource($this->currClientClsName));
$err = $this->makeReadable($err);
$err = trim ($err);
if ($err !== "") {
$errNo = mysql_errno($this->getClientResource($this->currClientClsName));
trigger_error(DbProxy::SQL_QUERY_FAILED . "\n" . $errNo . ": " . $err,
E_USER_ERROR);
}
}
$affectedRows = mysql_affected_rows ($this->getClientResource ($this->
currClientClsName));
} else {
if ($type !== $this->outputType_null) {
if ($type !== $this->outputType_boolean) {
while ($row = mysql_fetch_array ($result, MYSQL_NUM)) {
array_push($fetchedResult, $row);
}
}
$numRows = mysql_num_rows ($result);
$affectedRows = mysql_affected_rows ($this->getClientResource (
$this->currClientClsName));
mysql_freeresult($result);
} else {
mysql_freeresult($result);
return null;
}
}
switch ($type) {
case $this->outputType_boolean:
if ($result === false) {
return false;
} else {
$numericAnswer = max($affectedRows, $numRows);
if ($numericAnswer > 0) {
return true;
}
}
return false;
break;
case $this->outputType_number:
if (count ($fetchedResult) === 1) {
foreach ($fetchedResult as $row) {
if (count ($row) === 1) {
foreach ($row as $value) {
// Attempt to recognize the result as a numeric value:
$value = $this->convertNumericStr ($value);
// Case 1:
// Result is a numeric string literal:
if (is_int($value) || is_float($value)) {
return $value;
}
// Case 2:
// Result is not a numeric string literal; however, there's allways
// the chance that we're dealing with some number that was stored in
// the database AS A STRING, and hence, it was ENCODDED.
//
// Therefore, we attempt to decode it and will try to recognize it
// as a valid number one more time.
//
// NOTE:
// The encodding algorithm doesn't produce valid numerics, so we're
// on the safe side.
if (is_string ($value)) {
$value = $this->unicodeDecode ($value);
if (is_string ($value) && is_numeric ($value)) {
return $this->convertNumericStr ($value);
}
}
}
}
}
}
// The result itself failed to be interpreted as a number, so we fallback to our
// last resource of numeric information for this transaction:
$numericAnswer = max($affectedRows, $numRows);
return $numericAnswer;
break;
case $this->outputType_array:
if (count ($fetchedResult) > 0) {
for ($i=0; $i<count($fetchedResult); $i++) {
$decodedRow = array ();
$row = $fetchedResult[$i];
foreach ($row as $key => $value) {
if (is_string ($value)) { // can also be null
$value = $this->unicodeDecode($value);
}
$decodedRow [$this->unicodeDecode($key)] = $value;
}
$fetchedResult[$i] = $decodedRow;
}
}
return $fetchedResult;
break;
case $this->outputType_string:
if (count ($fetchedResult) === 1) {
foreach ($fetchedResult as $row) {
if (count ($row) === 1) {
foreach ($row as $value) {
if (is_string($value)) {
$value = $this->unicodeDecode($value);
return $value;
}
if (is_int ($value) || is_float ($value)) {
return "" . $value;
}
if (is_bool ($value)) {
return $value? "true" : "false";
}
}
}
}
} elseif (count ($fetchedResult) > 0) {
$ret = "";
for ($i=0; $i<count($fetchedResult); $i++) {
$row = $fetchedResult[$i];
foreach ($row as $key => $value) {
if (is_string($value)) {
$value = $this->unicodeDecode($value);
}
$key = $this->unicodeDecode($key);
if ($ret !== "") {
$ret .= "&";
}
$ret .= $key . "_" . $i . "=" . $value;
}
}
return $ret;
} elseif (max ($affectedRows, $numRows) > 0) {
if ($numRows > $affectedRows) {
return $numRows . " rows in set";
}
if ($affectedRows > $numRows) {
return $affectedRows . " rows affected";
}
} elseif ($fetchedResult === true) {
return DbProxy::SUCCESS;
}
return DbProxy::FAILURE;
break;
}
return null;
}
private function convertNumericStr ($value) {
$isNegative = $value[0] == "-";
$hasDecimalPart = (stripos($value,".") !== false);
$hasExponentialPart = (stripos($value,"e") !== false);
$converted = 0;
if ($hasDecimalPart || $hasExponentialPart) {
$converted = (float) $value;
} else {
$converted = (int) $value;
}
if ($isNegative && $converted > 0) {
$converted *= -1;
}
if ("" . $converted === $value) {
return $converted;
}
}
private function getUnicodePoints ($utf8String) {
return utf8ToUnicode ($utf8String);
}
private function getUtf8String ($unicodePoints) {
return unicodeToUtf8 ($unicodePoints);
}
private function unicodeEncode ($value) {
if (is_int($value) || is_float($value)) {
return $value;
}
$points = $this->getUnicodePoints($value);
$ret = array ();
for ($i=0; $i<count($points); $i++) {
$point = $points [$i];
$point = dechex ($point);
$point = strtoupper ($point);
$encPointChars = array ();
$encPoint = "";
/* Requirements:
* - first char must be an upercase letter, but not letter E
* - subsequent chars can be digits or upercase letters, as long as these letters are
* different than the subset of letters used by the first char
*/
for ($j=0; $j<strlen($point); $j++) {
$char = ord ($point [$j]);
if ($j == 0) {
// first char, digits:
if ($char >= 48 && $char <= 57) {
$encChar = $this->digitToLettersMap [$char];
} else {
// first char, A to F letters:
$char += 11;
$encChar = chr ($char);
}
} else {
// subsequent chars, digits (leave unchanged):
if ($char >= 48 && $char <= 57) {
$encChar = $point [$j];
} else {
// subsequent chars, A to F letters:
$char +=17;
$encChar = chr ($char);
}
}
$encPointChars[$j] = $encChar;
}
$encPoint = implode("", $encPointChars);
$ret[$i] = $encPoint;
}
return implode ("", $ret);
}
private function unicodeDecode ($value) {
if (is_int($value)) { // could be the key of an array
return $value;
} elseif (is_string($value)) {
if (is_numeric($value)) {
return $this->convertNumericStr ($value);
}
$tokens = array ();
$token = array ();
for ($i=0; $i<strlen($value); $i++) {
$chIndex = ord ($value[$i]);
// Upper letters A to K (except E) are the delimiters
if ($chIndex >= 65 && $chIndex <= 75) {
if (count ($token) !== 0) {
array_push ($tokens, strtoupper (implode ("", $token)));
$token = array ();
}
}
array_push($token, $value[$i]);
}
// add last token by hand
array_push ($tokens, strtoupper (implode ("", $token)));
$points = array ();
foreach ($tokens as $encToken) {
$unencToken = array ();
$unencChar = "";
for ($j=0; $j<strlen($encToken); $j++) {
$char = ord ($encToken [$j]);
if ($j == 0) {
// first char, uppercase letters, A to K (except E):
if ($char >= 65 && $char <= 75) {
$result = array_search(chr ($char), $this->digitToLettersMap, true);
$unencChar = chr ($result);
} else {
// first char, uppercase letters L to K
$unencChar = chr ($char - 11);
}
} else {
// subsequent chars, digits (accept as they are):
if ($char >= 48 && $char <= 57) {
$unencChar = chr ($char);
} else {
// subsequent chars, uppercase letters R to W
$unencChar = chr ($char - 17);
}
}
$unencToken [$j] = $unencChar;
}
$unencToken = implode ("", $unencToken);
array_push($points, hexdec($unencToken));
}
return $this->getUtf8String ($points);
}
return null;
}
private function makeReadable ($str) {
$matches = array ();
$pattern = $this->encIdentifierNamePatt;
preg_match_all($pattern, $str, $matches);
if (count ($matches) === 0) {
return $str;
}
foreach ($matches[0] as $match) {
$match = trim ($match, "\"'`");
$str = str_replace($match, $this->unicodeDecode($match), $str);
}
return $str;
}
private function getThisClassName () {
$ret = array ();
$name = get_class($this);
preg_match('/\w+$/', $name, $ret);
return $ret[0];
}
private function getParentClassName () {
$ret = array();
$name = get_parent_class($this);
preg_match('/\w+$/', $name, $ret);
return $ret[0];
}
private function safePassPattern () {
$patt = '';
for ($i=33; $i<127; $i++) {
if (array_search($i, $this->mysqlForbiddenChars, true) === false) {
$patt .= '\\x' . dechex($i);
}
}
$patt = '/^[' . $patt . ']+$/';
return $patt;
}
private function hostPattern () {
$patt = '/^(?:[a-z0-9])(?:[a-z0-9\-]{0,62}?)';
for ($i; $i<126; $i++) {
$patt .= '(?:\.(?:[a-z0-9\-]{1,62}?))?';
}
$patt .= '(?:\.(?:[a-z0-9\-]{0,62}?))?';
$patt .= '(?:[a-z0-9])$/';
return $patt;
}
private function makeEncIdentifierPattern () {
return "/(\\x22|\\x27|\\x60)[ABCDFGHIJKLMNOPQ][0123456789RSTUVW]{1}" .
str_repeat("(?:[ABCDFGHIJKLMNOPQ][0123456789RSTUVW]{1})?",
($this->identifierMaxChars - 2) / 2) . "\\1/";
}
}
?>