diff --git a/.gitignore b/.gitignore index 6798cb0..93c5fcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /.buildpath /.project +/log.html diff --git a/inc/db.php b/inc/db.php new file mode 100644 index 0000000..61b41d6 --- /dev/null +++ b/inc/db.php @@ -0,0 +1,655 @@ +array('table_name1'=>array('table_field1', 'table_field2', ...), 'table_name2'=>array(...)), + * 'types'=>array('field1'=>'field_type1', 'field2'=>'field_type2', ...) + * 'constraints'=>array('table_name1'=>'table_contraint1', 'table_name2'=>'table_contraint2', ...), + * 'cascading_delete'=>array('table_name1'=>array('linked_table1', 'linked_table2', ...), 'table_name2'=>...)) + * @var Array + */ + public function __construct($sDbServer, $sLogin, $sPass, $sDatabase, $asOptions, $sEncoding='utf8mb4') + { + parent::__construct(__CLASS__, Settings::DEBUG); + $this->sDatabase = $sDatabase; + $this->asOptions = $asOptions; + //$this->oConnection = mysql_connect(self::DB_SERVER, self::DB_LOGIN, self::DB_PASS); + $this->oConnection = new mysqli($sDbServer, $sLogin, $sPass); + $this->syncPhpParams($sEncoding); + + /* + $dsn = 'mysql:dbname='.$this->sDatabase.';host='.self::DB_SERVER; + try {$dbh = new PDO($dsn, self::DB_LOGIN, self::DB_PASS);} + catch (PDOException $e) {$this->addError('Connexion échouée : ' . $e->getMessage());} + */ + + $this->setTrace(false); + if($this->oConnection->connect_error) + { + $this->addError('bug connection : '.$this->oConnection->connect_error); + $this->sDbState = self::DB_NO_CONN; + } + else + { + if(!$this->oConnection->select_db($this->sDatabase)) + { + $this->addError('Could not find database "'.$this->sDatabase.'"'); + $this->sDbState = self::DB_NO_DATA; + } + else $this->sDbState = self::DB_PEACHY; + } + } + + private function syncPhpParams($sEncoding) + { + //Characters encoding + $this->oConnection->set_charset($sEncoding); //SET NAMES + + //Time zone + $oNow = new DateTime(); + $iMins = $oNow->getOffset() / 60; + $iSign = ($iMins < 0)?-1:1; + $iMins = abs($iMins); + $iHours = floor($iMins / 60); + $iMins -= $iHours * 60; + $sOffset = sprintf('%+d:%02d', $iHours*$iSign, $iMins); + $this->setQuery("SET time_zone='{$sOffset}';"); + } + + public function __destruct() + { + parent::__destruct(); + $this->oConnection->close(); + } + + public function setTrace($bTrace=true) + { + $this->bTrace = $bTrace; + if($bTrace) $this->setDebug(true); + } + + public function getTrace() + { + return $this->bTrace; + } + + public function getTables() + { + return array_keys($this->asOptions['tables']); + } + + public function install() + { + //Create Database + $this->setQuery("DROP DATABASE IF EXISTS ".$this->sDatabase); + $this->setQuery("CREATE DATABASE ".$this->sDatabase." DEFAULT CHARACTER SET ".Settings::DB_ENC." DEFAULT COLLATE ".Settings::DB_ENC."_general_ci"); + $this->oConnection->select_db($this->sDatabase); + + //Create tables + @array_walk($this->getInstallQueries(), array($this, 'setQuery')); + } + + //For debug purposes + public function getFullInstallQuery() + { + $asInstallQueries = $this->getInstallQueries(); + return str_replace("\n", "
", implode(";\n\n", $asInstallQueries))."\n\n"; + } + + private function getInstallQueries() + { + $asTables = $this->getTables(); + $asInstallQueries = array_map(array($this, 'getInstallQuery'), $asTables); + $asAlterQueries = $this->getForeignKeyQueries($asTables); + return array_merge($asInstallQueries, $asAlterQueries); + } + + private function getInstallQuery($sTableName) + { + $asTableColumns = $this->getTableColumns($sTableName); + $sQuery = "\n".$this->implodeAll($asTableColumns, "` ", "\n", "`", ",")."\n".implode(", \n", $this->getTableConstraints($sTableName)); + return "CREATE TABLE `{$sTableName}` ({$sQuery})"; + } + + private function getForeignKeyQueries($asTableNames) + { + $asForeignKeyQueries = array(); + foreach($asTableNames as $sTableName) + { + $asTableColumns = array_keys($this->getTablecolumns($sTableName)); + foreach($asTableColumns as $sColumnName) + { + if($this->isId($sColumnName) && $sColumnName!=self::getId($sTableName)) + { + $asForeignKeyQueries[] = "ALTER TABLE ".$sTableName." ADD INDEX(`".$sColumnName."`)"; + $asForeignKeyQueries[] = "ALTER TABLE ".$sTableName." ADD FOREIGN KEY (`".$sColumnName."`) REFERENCES ".self::getTable($sColumnName)."(`".$sColumnName."`)"; + } + } + } + return $asForeignKeyQueries; + } + + private function setQuery($sQuery, $sTypeQuery=__FUNCTION__) + { + $oResult = $this->getQuery($sQuery, $sTypeQuery); + return ($oResult!==false); + } + + private function getQuery($sQuery, $sTypeQuery=__FUNCTION__) + { + $sQuery = str_replace(array("\n", "\t"), array(" ", ""), $sQuery); + + if($this->getTrace()) $this->addNotice($sQuery.";"); + + if(!($oResult = $this->oConnection->query($sQuery))) + { + $this->addError("\nErreur SQL : \n".str_replace("\t", "", $sQuery.";")."\n\n".str_replace(array("\t", "\n"), "", $this->getLastError())); + } + return $oResult; + } + + public function getLastError() + { + return $this->oConnection->error; + } + + public function getArrayQuery($sQuery, $bStringOnly=false, $sGroupBy='', $sTypeQuery=__FUNCTION__) + { + $iIndex = 0; + $iColumnCount = 0; + $asResult = array(); + $oResult = $this->getQuery($sQuery, true, $sTypeQuery); + if($oResult!==false) + { + while($asCurrentRow = $oResult->fetch_array()) + { + if($bStringOnly) $asCurrentRow = $this->arrayKeyFilter($asCurrentRow, 'is_string'); + + //Add table reel keys + if($sGroupBy!='' && array_key_exists($sGroupBy, $asCurrentRow)) + { + $iRowKey = $asCurrentRow[$sGroupBy]; + unset($asCurrentRow[$sGroupBy]); + } + else $iRowKey = $iIndex; + + //For first loop, check table width + if($iIndex==0) $iColumnCount = count($asCurrentRow); + + //One column case : collapse a level + if($iColumnCount==1) $asCurrentRow = array_shift($asCurrentRow); + + $asResult[$iRowKey] = $asCurrentRow; + $iIndex++; + } + } + return $asResult; + } + + private function getMaxIncrementedValue($sTable) + { + return $this->selectValue($sTable, "MAX(".$this->getId($sTable).")"); + } + + public static function getId($sTableName, $bFull=false) + { + $sColumnName = self::ID_TAG.self::getText($sTableName); + return $bFull?self::getFullColumnName($sTableName, $sColumnName):$sColumnName; + } + + public static function getText($sTableName, $bFull=false) + { + $sColumnName = mb_substr(str_replace('`', '', $sTableName), 0, -1); + $sColumnName = mb_substr($sColumnName, -2)=='ie'?mb_substr($sColumnName, 0, -2).'y':$sColumnName; + return $bFull?self::getFullColumnName($sTableName, $sColumnName):$sColumnName; + } + + public static function getFullColumnName($sTableName, $sColumnName) + { + return $sTableName.".".$sColumnName; + } + + private function isId($sColumnName, $sTableName='') + { + $asTables = ($sTableName=='')?$this->getTables():array($sTableName); + $asTableIds = array_map(array('self', 'getId'), $asTables); + return in_array($sColumnName, $asTableIds); + } + + private function isField($sTableFieldName) + { + $asPath = explode('.', str_replace('`', '', $sTableFieldName)); + return (is_array($asPath) + && count($asPath)==2 + && in_array($asPath[0], $this->getTables()) //table among the declared tables + && in_array($asPath[1], array_keys($this->getTablecolumns($asPath[0])))); //field among the table fields + } + + private function getTable($sTableId) + { + $asTables = $this->getTables(); + $asTableIds = array_map(array('self', 'getId'), $asTables); + if(in_array($sTableId, $asTableIds)) return $asTables[array_search($sTableId, $asTableIds)]; + else + { + $this->addError('Id '.$sTableId.' présent dans aucune table'); + return false; + } + } + + public function getTablecolumns($sTableName) + { + if(!array_key_exists($sTableName, $this->asOptions['tables'])) return false; + + $asTableColumns = array(self::getId($sTableName)); + foreach($this->asOptions['tables'][$sTableName] as $sFieldName) $asTableColumns[] = $sFieldName; + $asTableColumns[] = 'led'; + + $asTableName = array_fill(0, count($asTableColumns), $sTableName); + return array_combine($asTableColumns, array_map(array('self', 'getColumnType'), $asTableColumns, $asTableName)); + } + + private function getColumnType($sColumnName, $sTableName) + { + $sColumnType = ''; + switch($sColumnName) + { + case array_key_exists($sColumnName, $this->asOptions['types']): + $sColumnType = $this->asOptions['types'][$sColumnName]; + break; + case 'led': + $sColumnType = "TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP"; + break; + case $this->isId($sColumnName, $sTableName): + $sColumnType = "int(10) UNSIGNED auto_increment"; + break; + case $this->isId($sColumnName): + $sColumnType = "int(10) UNSIGNED"; + break; + } + return $sColumnType; + } + + private function getTableConstraints($sTableName) + { + //Primary key + $asTableConstraints = array('PRIMARY' => "PRIMARY KEY (`".self::getId($sTableName)."`)"); + + //Foreign keys: applied using ALTER TABLE syntax at the end to prevent scheduling CREATE TABLE queries + + //Other constraints + if(array_key_exists($sTableName, $this->asOptions['constraints'])) $asTableConstraints[] = $this->asOptions['constraints'][$sTableName]; + return $asTableConstraints; + } + + private function addQuotes($oData) + { + //TODO remake + $asTrustedFunc = array('CURDATE()', 'NOW()'); + $sChar = "'"; + if(is_array($oData)) + { + $asChar = array_fill(1, count($oData), $sChar); + return array_combine(array_keys($oData), array_map(array($this, 'addQuotes'), $oData, $asChar)); + } + else + { + if(in_array($oData, $asTrustedFunc) || $this->isField($oData)) return $oData; + else return $sChar.$oData.$sChar; + } + } + + private function getLastId() + { + return $this->oConnection->insert_id; + } + + private function getLastImpact() + { + return ($this->oConnection->affected_rows > 0); + } + + public function insertRow($sTableName, $asData) + { + $this->cleanSql($sTableName); + $this->cleanSql($asData); + + $asQueryValues = $this->addQuotes($asData); + $sQuery = "INSERT INTO ".$sTableName." (`".implode("`, `", array_keys($asQueryValues))."`) VALUES (".implode(", ", $asQueryValues).")"; + return $this->setQuery($sQuery)?$this->getLastId():0; + } + + public function updateRow($sTableName, $asConstraints, $asData, $bLedUpdate=true) + { + return $this->updateRows($sTableName, $asConstraints, $asData, 1, $bLedUpdate); + } + + public function updateRows($sTableName, $asConstraints, $asData, $iLimit=0, $bLedUpdate=true) + { + if(!is_array($asConstraints)) + { + $asConstraints = array($this->getId($sTableName)=>$asConstraints); + } + + //Cleaning values + $this->cleanSql($sTableName); + $this->cleanSql($iTableId); + $this->cleanSql($asData); + $this->cleanSql($asConstraints); + $asQueryValues = $this->addQuotes($asData); + $asConstraintsValues = $this->addQuotes($asConstraints); + $this->addColumnSelectors($asQueryValues); + $this->addColumnSelectors($asConstraintsValues); + + //Building query + if(!$bLedUpdate) $asQueryValues['led'] = 'led'; + $sLimit = $iLimit>0?" LIMIT $iLimit":""; + $sQuery = "UPDATE {$sTableName} ". + "SET ".$this->implodeAll($asQueryValues, " = ", ", ")." ". + "WHERE ".$this->implodeAll($asConstraintsValues, " = ", " AND ").$sLimit; + + $iResult = false; + if($this->setQuery($sQuery)) + { + if(!$this->getLastImpact()) $this->addNotice('Last query had no effect on db: "'.$sQuery.';"'); + $iResult = ($iLimit==1)?$this->selectValue($sTableName, $this->getId($sTableName), $asConstraints):true; + } + return $iResult; + } + + public function insertUpdateRow($sTableName, $asData, $asKeys=array(), $bUpdate=true) + { + $sTableIdName = self::getId($sTableName); + + //check for data in the db + if($asKeys==array()) + { + $asKeys[] = $sTableIdName; + } + $asValues = array_intersect_key($asData, array_flip($asKeys)); + $iTableId = $this->selectValue($sTableName, $sTableIdName, $asValues); + + //insert + if(!$iTableId) + { + $iTableId = $this->insertRow($sTableName, $asData); + } + //Update + elseif($bUpdate) + { + if(array_key_exists($sTableIdName, $asData)) + { + unset($asData[$sTableIdName]); + } + $iTableId = $this->updateRow($sTableName, $iTableId, $asData); + } + return $iTableId; + } + + public function selectInsert($sTableName, $asData, $asKeys=array()) + { + return $this->insertUpdateRow($sTableName, $asData, $asKeys, false); + } + + public function deleteRow($sTableName, $iTableId) + { + $this->cleanSql($sTableName); + $this->cleanSql($iTableId); + + $bSuccess = true; + + //linked tables + switch($sTableName) + { + case array_key_exists($sTableName, $this->asOptions['cascading_delete']) : + $asTables = array_merge(is_array($sTableName)?array_values($sTableName):array($sTableName), array_values($this->asOptions['cascading_delete'][$sTableName])); + break; + case is_string($sTableName) : + $asTables = array($sTableName); + break; + case is_array($sTableName): + $asTables = $sTableName; + break; + default: + $asTables = array(); + } + foreach($asTables as $sTable) + { + $bSuccess = $bSuccess && $this->setQuery("DELETE FROM ".$sTable." WHERE ".$this->getId($sTableName)." = ".$iTableId); + } + return $bSuccess; + } + + public function emptyTable($sTableName) + { + $this->cleanSql($sTableName); + return $this->setQuery("TRUNCATE ".$sTableName); + } + + public function selectList($sTableName, $sColumnName='', $asConstraints=array()) + { + $sColumnName = $sColumnName==''?self::getText($sTableName):$sColumnName; + $sIdColumnName = self::getId($sTableName); + return $this->selectRows( array( 'select' => array($sIdColumnName, $sColumnName), + 'from' => $sTableName, + 'constraint'=> $asConstraints), + true, + $sIdColumnName); + } + + public function selectRows($asInfo, $sGroupBy='', $bStringOnly=true) + { + $asAttributes = array('select'=>"SELECT", 'from'=>"FROM", 'join'=>"LEFT JOIN", 'joinOn'=>"LEFT JOIN", 'constraint'=>"WHERE", 'groupBy'=>"GROUP BY", 'orderBy'=>"ORDER BY", 'limit'=>'LIMIT'); + $asRowSeparators = array('select'=>", ", 'from'=>"", 'join'=>" LEFT JOIN ", 'joinOn'=>" LEFT JOIN ", 'constraint'=>" AND ", 'groupBy'=>", ", 'orderBy'=>", ", 'limit'=>""); + $asOperators = array('constraint'=>" = ", 'orderBy'=>" ", 'join'=>" USING(", 'joinOn'=>" ON "); + $asEndOfStatement = array('constraint'=>"", 'orderBy'=>"", 'join'=>")", 'joinOn'=>""); + + //Simple selectRows + if(!is_array($asInfo)) $asInfo = array('from'=>$asInfo); + + //Get table by key + if($sGroupBy===true) + { + $sGroupBy = self::getId($asInfo['from']); + + //Add id to selection + if(isset($asInfo['select']) && $asInfo['select'][0]!="*") $asInfo['select'][] = $sGroupBy; + } + + $sQuery = ""; + foreach($asAttributes as $sStatement => $sKeyWord) + { + $asSelection = array_key_exists($sStatement, $asInfo)?$asInfo[$sStatement]:array(); + if(!is_array($asSelection)) + { + $asSelection = array($asSelection); + } + + //if provided values + if(!empty($asSelection)) + { + $this->cleanSql($asSelection); + + if($sStatement=='constraint' && !array_key_exists('constVar', $asInfo)) + { + $asSelection = $this->addQuotes($asSelection); + foreach($asSelection as $sField=>$asConstraints) + { + if(is_array($asConstraints)) + { + $asSelection[$sField] = "(".implode(', ', $asConstraints).")"; + $asInfo['constOpe'][$sField] = " IN "; + } + elseif(!array_key_exists('constOpe', $asInfo) || !array_key_exists($sField, $asInfo['constOpe'])) $asInfo['constOpe'][$sField] = " = "; + } + } + $this->addColumnSelectors($asSelection); + + $sQuery .= " ".$sKeyWord." "; + + //in case of double value input + if(array_key_exists($sStatement, $asOperators)) + { + if($sStatement=='constraint' && array_key_exists('constOpe', $asInfo)) + { + $asOperators[$sStatement] = $asInfo['constOpe']; + } + elseif($sStatement=='joinOn') + { + $asSimplifiedSelection = array(); + foreach($asSelection as $sTable => $asJoinFields) + { + $asJoinFields = $this->addQuotes($asJoinFields); + $asSimplifiedSelection[$sTable] = $this->implodeAll($asJoinFields, " = ", " AND "); + } + $asSelection = $asSimplifiedSelection; + } + $sQuery .= $this->implodeAll($asSelection, $asOperators[$sStatement], $asRowSeparators[$sStatement], "", $asEndOfStatement[$sStatement]); + } + else + { + $sQuery .= implode($asRowSeparators[$sStatement], $asSelection); + } + } + //default value for select + elseif($sStatement=='select') + { + $sQuery .= " ".$sKeyWord." * "; + } + } + return $this->getArrayQuery(trim($sQuery), $bStringOnly, $sGroupBy); + } + + private function addColumnSelectors(&$asSelection) + { + //FIXME get rid of this + $sSqlWord = 'option'; + $sKey = array_search($sSqlWord, $asSelection); + if($sKey!==false) + { + $asSelection[$sKey] = "`".$asSelection[$sKey]."`"; + } + elseif(array_key_exists($sSqlWord, $asSelection)) + { + $asSelection["`".$sSqlWord."`"] = $asSelection[$sSqlWord]; + unset($asSelection[$sSqlWord]); + } + } + + public function selectRow($sTableName, $asConstraints=array(), $sColumnName='*') + { + //Table ID directly + if(!is_array($asConstraints)) $asConstraints = array($this->getId($sTableName)=>$asConstraints); + + $asRows = $this->selectRows(array('select'=>$sColumnName, 'from'=>$sTableName, 'constraint'=>$asConstraints)); + $iCountNb = count($asRows); + switch($iCountNb) + { + case 0 : + $asResult = array(); + break; + case $iCountNb > 1 : + $this->addError('More than 1 result for a selectRow(): '.$iCountNb.' results. Table: '.$sTableName.', constraint: '.self::implodeAll($asConstraints, '=', ' ')); + default: + $asResult = array_shift($asRows); + } + return $asResult; + } + + public function selectValue($sTableName, $sColumnName, $oConstraints=array()) + { + if(!is_array($oConstraints)) + { + $oConstraints = array($this->getId($sTableName)=>$oConstraints); + } + $oResult = $this->selectRow($sTableName, $oConstraints, $sColumnName); + return empty($oResult)?false:$oResult; + } + + public function pingValue($sTableName, $oConstraints) + { + return $this->selectValue($sTableName, 'COUNT(1)', $oConstraints); + } + + public function cleanSql(&$oData) + { + $this->cleanData($oData); + $oData = $this->cleanData($oData); + } + + //TODO déplacer dans ToolBox::implodeAll + public static function implodeAll($asText, $asKeyValueSeparator='', $sRowSeparator='', $sKeyPre='', $sValuePost=false) + { + if($sValuePost===false) + { + $sValuePost = $sKeyPre; + } + $asCombinedText = array(); + + //if unique value for key value separator + if(!is_array($asKeyValueSeparator) && !empty($asText)) + { + $asKeyValueSeparator = array_combine(array_keys($asText), array_fill(0, count($asText), $asKeyValueSeparator)); + } + + $asFrom = array('[/KEY\]', '[/VALUE\]'); + foreach($asText as $sKey=>$sValue) + { + $asTo = array($sKey, $sValue); + $sRepKeyPre = str_replace($asFrom, $asTo, $sKeyPre); + $asRepKeyValueSeparator = str_replace($asFrom, $asTo, $asKeyValueSeparator[$sKey]); + $sRepValuePost = str_replace($asFrom, $asTo, $sValuePost); + $asCombinedText[] = $sRepKeyPre.$sKey.$asRepKeyValueSeparator.(is_array($sValue)?implode($sValue):$sValue).$sRepValuePost; + } + return implode($sRowSeparator, $asCombinedText); + } + + public static function arrayKeyFilter($asArray, $sCallBack) + { + $asValidKeys = array_flip(array_filter(array_keys($asArray), $sCallBack)); + return array_intersect_key($asArray, $asValidKeys); + } + + public function cleanData($oData) + { + if(!is_array($oData)) + { + return $this->oConnection->real_escape_string($oData); + } + elseif(count($oData)>0) + { + $asKeys = array_map(array($this, 'cleanData'), array_keys($oData)); + $asValues = array_map(array($this, 'cleanData'), $oData); + return array_combine($asKeys, $asValues); + } + } +} + +?> \ No newline at end of file