diff --git a/inc/Db.php b/inc/Db.php
new file mode 100644
index 0000000..c943278
--- /dev/null
+++ b/inc/Db.php
@@ -0,0 +1,753 @@
+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($asConf, $asOptions)
+ {
+ $this->asConf = $asConf;
+ $this->asOptions = $asOptions;
+
+ parent::__construct(__FILE__, Settings::DEBUG);
+ $this->oConnection = new \mysqli($this->getConf('server'), $this->getConf('user'), $this->getConf('pass'));
+ $this->syncPhpParams($this->getConf('encoding'));
+
+ $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->getConf('database')))
+ {
+ $this->addError('Could not find database "'.$this->sDatabase.'"');
+ $this->sDbState = self::DB_NO_DATA;
+ }
+ elseif(empty($this->getArrayQuery("SHOW TABLES")))
+ {
+ $this->sDbState = self::DB_NO_TABLE;
+ }
+ else $this->sDbState = self::DB_PEACHY;
+ }
+ }
+
+ private function getConf($sConf) {
+ return $this->asConf[$sConf] ?? null;
+ }
+
+ private function syncPhpParams($sEncoding)
+ {
+ //Characters encoding
+ $this->oConnection->set_charset($sEncoding); //SET NAMES
+
+ //Timezone
+ $this->setQuery("SET time_zone='".date_default_timezone_get()."'");
+ }
+
+ public function __destruct()
+ {
+ parent::__destruct();
+ $this->oConnection->close();
+ }
+
+ public function setTrace($bTrace=true)
+ {
+ $this->bTrace = $bTrace;
+ }
+
+ 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 ".$this->getConf('encoding')." DEFAULT COLLATE ".$this->getConf('encoding')."_general_ci");
+ $this->oConnection->select_db($this->sDatabase);
+
+ //Create tables
+ @array_walk($this->getInstallQueries(), array($this, 'setQuery'));
+ }
+
+ public function getBackup() {
+ $sBackupFile = uniqid('backup_').'.sql';
+
+ $sAppPath = '';
+ if(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') $sAppPath = 'C:\ProgramData\xampp\mysql\bin\\';
+ exec($sAppPath.'mysqldump --user='.$this->getConf('user').' --password='.$this->getConf('pass').' '.$this->getConf('database').' --add-drop-table --result-file='.$sBackupFile);
+ if(file_exists($sBackupFile)) {
+ $sBackup = file_get_contents($sBackupFile);
+ unlink($sBackupFile);
+ return $sBackup;
+ }
+ else return false;
+ }
+
+ public function restoreBackup($sBackupFile) {
+ $sAppPath = '';
+ if(file_exists($sBackupFile)) {
+ if(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') $sAppPath = 'C:\ProgramData\xampp\mysql\bin\\';
+ return exec($sAppPath.'mysql --user='.$this->getConf('user').' --password='.$this->getConf('pass').' '.$this->getConf('database').' < '.$sBackupFile);
+ }
+ else return false;
+ }
+
+ public function loadFile($sFilePath) {
+ set_time_limit(0);
+ $bResult = false;
+
+ if(file_exists($sFilePath))
+ {
+ $sContent = file_get_contents($sFilePath);
+ $sContent = ToolBox::fixEOL($sContent);
+ if(str_replace(ToolBox::FILE_EOL, '', $sContent)!='')
+ {
+ $asLines = explode(ToolBox::FILE_EOL, $sContent);
+ $sSql = '';
+ foreach ($asLines as $sLine)
+ {
+ $sSql .= trim($sLine);
+ if(substr($sSql, -1)==';') //Multi line SQL
+ {
+ $asResult = $this->setQuery($sSql);
+ if($asResult === false)
+ {
+ $this->addError('SQL failed with error: '.$this->db->error, $sSql);
+ $bResult = false;
+ break;
+ }
+
+ $bResult = true;
+ $sSql = '';
+ }
+ else $sSql .= " ";
+ }
+ }
+ else $this->addError('File is empty: '.basename($sFilePath));
+ }
+ else $this->addError('File not found: '.$sFilePath);
+
+ return $bResult;
+ }
+
+ //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 = $this->getTablecolumns($sTableName, false);
+ 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
+ && $this->isColumnInTable($asPath[0], $asPath[1])
+ );
+ }
+
+ 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, $bTypes=true)
+ {
+ 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';
+
+ if(!$bTypes) return $asTableColumns;
+
+ $asTableName = array_fill(0, count($asTableColumns), $sTableName);
+ return array_combine($asTableColumns, array_map(array('self', 'getColumnType'), $asTableColumns, $asTableName));
+ }
+
+ public function isColumnInTable($sTableName, $sColName) {
+ $asCols = $this->getTablecolumns($sTableName, false);
+ return ($asCols && in_array($sColName, $asCols));
+ }
+
+ 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 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'])) {
+ if(is_array($this->asOptions['constraints'][$sTableName])) $asTableConstraints = array_merge($asTableConstraints, $this->asOptions['constraints'][$sTableName]);
+ else $asTableConstraints[] = $this->asOptions['constraints'][$sTableName];
+ }
+ return $asTableConstraints;
+ }
+
+ private function addQuotes($oData)
+ {
+ //TODO remake
+ $asTrustedFunc = array('CURDATE()', 'NOW()', 'NULL');
+ $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($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('cascading_delete', $this->asOptions) && 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))
+ {
+ if(array_key_exists('constOpe', $asInfo) && array_key_exists($sField, $asInfo['constOpe']) && $asInfo['constOpe'][$sField]=='BETWEEN') {
+ //Between
+ $asSelection[$sField] = $asConstraints['from'].' AND '.$asConstraints['to'];
+ $asInfo['constOpe'][$sField] = " BETWEEN ";
+ }
+ else {
+ //Multiple values (IN)
+ $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 selectColumn($sTableName, $asColumnNames, $asConstraints)
+ {
+ $sGroupBy = '';
+ if(!is_array($asColumnNames)) $asColumnNames = array($asColumnNames);
+ else $sGroupBy = $asColumnNames[0];
+
+ return $this->selectRows(
+ array(
+ 'select' => $asColumnNames,
+ 'from' => $sTableName,
+ 'constraint'=> $asConstraints
+ ),
+ $sGroupBy
+ );
+ }
+
+ 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 selectId($sTableName, $oConstraints)
+ {
+ return $this->selectValue($sTableName, self::getId($sTableName), $oConstraints);
+ }
+
+ 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);
+ }
+ }
+}
diff --git a/inc/rss.php b/inc/Feed.php
old mode 100755
new mode 100644
similarity index 100%
rename from inc/rss.php
rename to inc/Feed.php
diff --git a/inc/Main.php b/inc/Main.php
new file mode 100644
index 0000000..9a4b5c4
--- /dev/null
+++ b/inc/Main.php
@@ -0,0 +1,181 @@
+setContext($sProcessPage, $sTimeZone);
+
+ //Init objects
+ if($bDb) {
+ $asConf = array(
+ 'server' => Settings::DB_SERVER,
+ 'database' => Settings::DB_NAME,
+ 'user' => Settings::DB_LOGIN,
+ 'pass' => Settings::DB_PASS,
+ 'encoding' => Settings::DB_ENC
+ );
+ $this->oDb = new Db($asConf, $this->getSqlOptions());
+ if(in_array($this->oDb->sDbState, array(Db::DB_NO_DATA, Db::DB_NO_TABLE))) $this->install();
+ }
+ }
+
+ protected abstract function install();
+
+ public function getDbBuildScript() {
+ return $this->oDb->getFullInstallQuery();
+ }
+
+ private function setContext($sProcessPage, $sTimeZone='')
+ {
+ //Browser <> PHP <> MySql synchronization
+ if($sTimeZone=='') $sTimeZone = Settings::TIMEZONE;
+ date_default_timezone_set($sTimeZone);
+ ini_set('default_charset', Settings::TEXT_ENC);
+ header('Content-Type: text/html; charset='.Settings::TEXT_ENC);
+ mb_internal_encoding(Settings::TEXT_ENC);
+ mb_http_output(Settings::TEXT_ENC);
+ mb_http_input(Settings::TEXT_ENC);
+ mb_language('uni');
+ mb_regex_encoding(Settings::TEXT_ENC);
+
+ $this->asContext['process_page'] = basename($sProcessPage);
+
+ $sServerName = array_key_exists('SERVER_NAME', $_SERVER)?$_SERVER['SERVER_NAME']:$_SERVER['PWD'];
+ $sScheme = array_key_exists('HTTP_X_FORWARDED_PROTO', $_SERVER)?$_SERVER['HTTP_X_FORWARDED_PROTO']:$_SERVER['REQUEST_SCHEME'];
+ $sAppPath = $sScheme.'://'.str_replace(array('http://', 'https://'), '', $sServerName.dirname($_SERVER['SCRIPT_NAME']));
+ $this->asContext['serv_name'] = $sAppPath.(mb_substr($sAppPath, -1)!='/'?'/':'');
+
+ $this->setMasks();
+ }
+
+ public static function addTimestampToFilePath($sFilePath)
+ {
+ //Remove timestamp
+ $sCleanedFilePath = preg_replace('/(.*)\?[\d]{14}$/', '$1', $sFilePath);
+
+ //Add timestamp
+ return file_exists($sCleanedFilePath)?$sCleanedFilePath.'?'.date("YmdHis", filemtime($sCleanedFilePath)):$sFilePath;
+ }
+
+ public function addUncaughtError($sError)
+ {
+ $this->addError('Uncaught errors:'."\n".$sError);
+ }
+
+ /* Building main pages */
+
+ public function getMainPage($asGlobalVars=array(), $sMainPage='index', $asMainPageTags=array(), $asCachePages=array())
+ {
+ $asDefaultConsts = array('success'=>self::SUCCESS, 'error'=>self::ERROR, 'process_page'=>$this->asContext['process_page']);
+ $asGlobalVars['consts'] = array_merge($asDefaultConsts, array_key_exists('consts', $asGlobalVars)?$asGlobalVars['consts']:array());
+
+ //Masks
+ if(empty($asCachePages)) $asCachePages = array_values($this->asMasks);
+ foreach($asCachePages as $sPage)
+ {
+ if($sPage != $sMainPage) {
+ $oMask = new Mask($sPage, $this->oLang);
+ $sPageContent = $oMask->getMask();
+ $asGlobalVars['consts']['pages'][$sPage] = $sPageContent;
+ }
+ }
+
+ if(!is_null($this->oLang)) {
+ $asGlobalVars['consts']['lang_prefix'] = Mask::LANG_PREFIX;
+ $asGlobalVars['consts']['lang'] = $this->oLang->getTranslations();
+ }
+
+ $oMainMask = new Mask($sMainPage, $this->oLang);
+ $oMainMask->setTag('GLOBAL_VARS', json_encode($asGlobalVars));
+ $oMainMask->setTags($asMainPageTags);
+
+ return $oMainMask->getMask();
+ }
+
+ protected function getPageContent($sPage)
+ {
+ return ToolBox::fixEOL(file_get_contents(self::MASKS_FOLDER.$sPage.'.html'));
+ }
+
+ /* DB structure. See Db::__construct */
+
+ protected abstract function getSqlOptions();
+
+ private function setMasks()
+ {
+ //List all available masks
+ $asMaskPaths = glob(Mask::getMaskFile('*'));
+ $this->asMasks = array_map('basename', $asMaskPaths, array_fill(1, count($asMaskPaths), Mask::MASK_EXT));
+ }
+
+ public static function getJsonResult($bSuccess, $sDesc, $asVars=array())
+ {
+ header('Content-type: application/json');
+ $asResult = self::getResult($bSuccess, $sDesc, $asVars);
+ $asResult['result'] = $asResult['result']?self::SUCCESS:self::ERROR;
+ return json_encode($asResult);
+ }
+
+ public static function getResult($bSuccess, $sDesc, $asVars=array()) {
+ return array('result'=>$bSuccess, 'desc'=>$sDesc, 'data'=>$asVars);
+ }
+
+ public static function goTo403() {
+ http_response_code(403);
+ exit;
+ }
+
+ public static function goTo404() {
+ http_response_code(404);
+ exit;
+ }
+}
diff --git a/inc/Mask.php b/inc/Mask.php
new file mode 100644
index 0000000..c2e06b7
--- /dev/null
+++ b/inc/Mask.php
@@ -0,0 +1,316 @@
+sMaskName = '';
+ $this->sFilePath = '';
+ $this->sMask = '';
+ $this->asTags = array();
+ $this->asTagsParams = array();
+ $this->asPartsSource = array();
+ $this->aoInstances = array();
+ $this->sFilePath = '';
+ $this->setTranslator($oLang);
+ $this->setTimezone($sTimezone);
+
+ //load file
+ if($sFileName!='') $this->initFile($sFileName);
+ }
+
+ public function setLanguage($sLang, $sDefaultLang='') {
+ if(is_null($this->oLang)) $this->setTranslator(new Translator($sLang, $sDefaultLang));
+ else {
+ $this->oLang->setLanguage($sLang, $sDefaultLang);
+ foreach($this->aoInstances as &$aoPartInstance) {
+ foreach($aoPartInstance as $oInstance) $oInstance->setLanguage($sLang, $sDefaultLang);
+ }
+ }
+ }
+
+ public function setTimezone($sTimezone) {
+ $this->sTimezone = ($sTimezone=='')?Settings::TIMEZONE:$sTimezone;
+ }
+
+ private function setTranslator($oLang) {
+ $this->oLang = $oLang;
+ foreach($this->aoInstances as &$aoPartInstance) {
+ foreach($aoPartInstance as $oInstance) $oInstance->setTranslator($this->oLang);
+ }
+ }
+
+ /**
+ * Get Mask Translator
+ * @return Translator
+ */
+ public function getTranslator() {
+ return $this->oLang;
+ }
+
+ public static function getMaskFile($sFileName)
+ {
+ return self::MASK_FOLDER.$sFileName.self::MASK_EXT;
+ }
+
+ public function initFile($sFileName)
+ {
+ $sFilePath = self::getMaskFile(basename($sFileName));
+ if(file_exists($sFilePath))
+ {
+ $this->sFilePath = $sFilePath;
+ $sSource = file_get_contents($this->sFilePath);
+ $this->initMask($sFileName, $sSource);
+ }
+ else
+ {
+ $this->addError('Fichier introuvable à l\'adresse : '.$sFilePath);
+ }
+ }
+
+ public function initFileFromString($sSource, $sPartName='', $iInstanceNb=0)
+ {
+ $this->initMask($sPartName.' (from row) '.$iInstanceNb, $sSource);
+ }
+
+ private function initMask($sMaskName, $sSource)
+ {
+ $this->sMaskName = $sMaskName;
+ $this->sMask = $sSource;
+ $this->setParts();
+ $this->setDefaultTagValues();
+ }
+
+ private function setParts()
+ {
+ while(preg_match('/\<\!-- \[PART\] (?P\S+) \[START\] --\>/u', $this->sMask, $asMatch))
+ {
+ $sPartName = $asMatch['part'];
+
+ $this->asPartsSource[$sPartName] = $this->getCleanPart($sPartName);
+ $this->aoInstances[$sPartName] = array();
+ }
+ }
+
+ private function getCleanPart($sPartName)
+ {
+ $iStartPos = $this->getPartStartPos($sPartName);
+ $iEndPos = $this->getPartEndPos($sPartName);
+ $sPart = mb_substr($this->sMask, $iStartPos, $iEndPos-$iStartPos);
+ $sExtendedPart = $this->getPartPattern($sPartName, self::START_TAG).$sPart. $this->getPartPattern($sPartName, self::END_TAG);
+ $this->sMask = str_replace($sExtendedPart, $this->getPartTagPattern($sPartName), $this->sMask);
+ return $sPart;
+ }
+
+ private function getPartStartPos($sPartName)
+ {
+ $sPartStartPattern = $this->getPartPattern($sPartName, self::START_TAG);
+ return mb_strpos($this->sMask, $sPartStartPattern) + strlen($sPartStartPattern);
+ }
+
+ private function getPartEndPos($sPartName)
+ {
+ $sPartEndPattern = $this->getPartPattern($sPartName, self::END_TAG);
+ return mb_strpos($this->sMask, $sPartEndPattern);
+ }
+
+ private function getPartPattern($sPartName, $sAction)
+ {
+ return '';
+ }
+
+ private function getPartTagPattern($sPartName, $bMark=true)
+ {
+ $sPartTag = 'PART '.$sPartName;
+ return $bMark?$this->addTagMark($sPartTag):$sPartTag;
+ }
+
+ public function addInstance($sPartName, $asTags)
+ {
+ $this->newInstance($sPartName);
+ foreach($asTags as $sTagName=>$sTagValue)
+ {
+ $this->setInstanceTag($sPartName, $sTagName, $sTagValue);
+ }
+ }
+
+ public function newInstance($sPartName)
+ {
+ //Finding the part ($oMask is a pointer)
+ $oMask = $this->findPart($this, $sPartName);
+ if(!$oMask) $this->addError('No part found : '.$sPartName);
+
+ //Retrieving source html
+ $sPartSource = $oMask->asPartsSource[$sPartName];
+
+ //Creating new instance
+ $oInstance = new Mask('', $this->oLang);
+ $oInstance->initFileFromString($sPartSource, $sPartName);
+ $oMask->aoInstances[$sPartName][] = $oInstance;
+ }
+
+ public function setInstanceTag($sPartName, $sTagName, $sTagValue, $asTagParams=array())
+ {
+ $oMask = $this->findPart($this, $sPartName);
+ if(!$oMask) $this->addError('No part found : '.$sPartName);
+
+ $oMask->getCurrentInstance($sPartName)->setTag($sTagName, $sTagValue, $asTagParams);
+ }
+
+ public function setInstanceTags($sPartName, $asTags)
+ {
+ $oMask = $this->findPart($this, $sPartName);
+ if(!$oMask) $this->addError('No part found : '.$sPartName);
+
+ foreach($asTags as $sTagName=>$sTagValue) {
+ $oMask->getCurrentInstance($sPartName)->setTag($sTagName, $sTagValue);
+ }
+ }
+
+ private function findPart($oMask, $sPartName)
+ {
+ if(array_key_exists($sPartName, $oMask->aoInstances))
+ {
+ return $oMask;
+ }
+ else //going deeper to find the part
+ {
+ foreach($oMask->aoInstances as $sLevelPartName=>$aoInstances)
+ {
+ if(!empty($aoInstances))
+ {
+ //take last instances
+ $oTmpMask = $this->findPart($oMask->getCurrentInstance($sLevelPartName), $sPartName);
+ if($oTmpMask) return $oTmpMask;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * returns latest instance
+ * @param String $sPartName
+ * @return Mask
+ */
+ private function getCurrentInstance($sPartName)
+ {
+ if(!empty($this->aoInstances[$sPartName]))
+ {
+ return end($this->aoInstances[$sPartName]);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ private function setDefaultTagValues() {
+ $asTagNames = $this->getTags();
+
+ //Default translations
+ foreach($asTagNames as $sTagName) {
+ if(self::isLangTag($sTagName)) $this->setTag($sTagName, $sTagName);
+ }
+ }
+
+ public function getTags()
+ {
+ $asMatches = array('tag'=>array());
+ $sSafeTagMark = preg_quote(self::TAG_MARK);
+ $sSafeLangMark = preg_quote(self::LANG_PREFIX);
+ $sPattern = '/'.$sSafeTagMark.'(?P('.$sSafeLangMark.'|)[\w\-]+)'.$sSafeTagMark.'/u';
+ preg_match_all($sPattern, $this->sMask, $asMatches);
+ return array_unique(array_filter($asMatches['tag']));
+ }
+
+ public function setTag($sTagName, $sTagValue, $asTagParams=array())
+ {
+ $this->asTags[$sTagName] = $sTagValue;
+ $this->asTagsParams[$sTagName] = $asTagParams;
+ }
+
+ public function setTags($asTags)
+ {
+ foreach($asTags as $sTagName=>$sTagValue) $this->setTag($sTagName, $sTagValue);
+ }
+
+ private static function isLangTag($sTag) {
+ return (mb_substr($sTag, 0, 5) == self::LANG_PREFIX);
+ }
+
+ private static function isTimeTag($sTag) {
+ return (mb_substr($sTag, 0, 5) == self::TIME_PREFIX);
+ }
+
+ public function getMask()
+ {
+ $sCompletedMask = $this->sMask;
+
+ //Build Parts
+ foreach($this->aoInstances as $sPart=>$aoParts)
+ {
+ $sTagValue = '';
+ foreach($aoParts as $oInstance)
+ {
+ $sTagValue .= $oInstance->getMask();
+ }
+ $this->setTag($this->getPartTagPattern($sPart, false), $sTagValue);
+ }
+
+ //Special Tags
+ $asTags = $this->asTags;
+ foreach($asTags as $sTagName=>&$sTagValue) {
+ $sTagActValue = mb_substr($sTagValue, 5);
+
+ //Translate Tag
+ if(!is_null($this->oLang) && self::isLangTag($sTagValue)) $sTagValue = $this->oLang->getTranslation($sTagActValue, $this->asTagsParams[$sTagName]);
+
+ //Convert Value to Mask Time Zone
+ if(self::isTimeTag($sTagValue)) $sTagValue = (new \DateTime('@'.$sTagActValue))->setTimeZone(new \DateTimeZone($this->sTimezone))->format($this->asTagsParams[$sTagName]);;
+ }
+
+ //Replace Tags
+ if(!empty($asTags)) {
+ $asMarkedTags = $this->addTagMark(array_keys($asTags));
+ $sCompletedMask = str_replace($asMarkedTags, $asTags, $sCompletedMask);
+ }
+ return $sCompletedMask;
+ }
+
+ private function addTagMark($oData)
+ {
+ return ToolBox::array_map_encapsulate($oData, self::TAG_MARK);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/inc/PhpObject.php b/inc/PhpObject.php
new file mode 100644
index 0000000..40f7eb8
--- /dev/null
+++ b/inc/PhpObject.php
@@ -0,0 +1,192 @@
+asMessageStack = array();
+ $this->resetMessageStack();
+ $this->setDebug($bDebug);
+ $this->setExtractMode($iExtractMode);
+ $this->sChildClass = $sClass;
+ }
+
+ public function setExtractMode($iExtractMode) {
+ $this->iExtractMode = $iExtractMode;
+ }
+
+ /* private function, use Settings::DEBUG for global setting */
+ private function setDebug($bDebug)
+ {
+ $this->bDebug = $bDebug;
+ }
+
+ public function getDebug()
+ {
+ return $this->bDebug;
+ }
+
+ private static function getLogPath()
+ {
+ return dirname(__FILE__).'/'.self::LOG_FILENAME;
+ }
+
+ private function resetMessageStack($sType=self::ALL_TAB)
+ {
+ if($sType==self::ALL_TAB)
+ {
+ $this->resetMessageStack(self::NOTICE_TAB);
+ $this->resetMessageStack(self::WARNING_TAB);
+ $this->resetMessageStack(self::ERROR_TAB);
+ }
+ else
+ {
+ $this->asMessageStack[$sType] = array();
+ }
+ }
+
+ protected function addNotice($sNotice)
+ {
+ $this->addMessage(self::NOTICE_TAB, $sNotice);
+ }
+
+ protected function addWarning($sWarning)
+ {
+ $this->addMessage(self::WARNING_TAB, $sWarning);
+ }
+
+ protected function addError($sError)
+ {
+ $this->addMessage(self::ERROR_TAB, $sError);
+ }
+
+ private function addMessage($sType, $sMessage)
+ {
+ $this->asMessageStack[$sType][] = array('msg'=>$sMessage, 'time'=>date('d.m.Y H:i:s'), 'type'=>$sType, 'location'=>$this->sChildClass);
+ }
+
+ protected function getCleanMessageStack($sType=self::ALL_TAB)
+ {
+ if($sType==self::ALL_TAB) {
+ $asMessages = array_merge( //No overlap, numeric keys
+ $this->asMessageStack[self::NOTICE_TAB],
+ $this->asMessageStack[self::WARNING_TAB],
+ $this->asMessageStack[self::ERROR_TAB]
+ );
+ }
+ else $asMessages = $this->asMessageStack[$sType];
+
+ $this->resetMessageStack($sType);
+ return $this->glueMessages($asMessages);
+ }
+
+ protected function cleanMessageStack()
+ {
+ $sErrorStack = $this->getCleanMessageStack($this->getDebug()?self::ALL_TAB:self::ERROR_TAB);
+ if($sErrorStack!='')
+ {
+ switch($this->iExtractMode)
+ {
+ case self::MODE_TEXT:
+ echo $sErrorStack;
+ break;
+ case self::MODE_HTML:
+ echo $sErrorStack;
+ break;
+ case self::MODE_ARRAY:
+ break;
+ case self::MODE_FILE:
+ @file_put_contents(self::getLogPath(), "\n".$sErrorStack, FILE_APPEND);
+ break;
+ }
+ }
+ }
+
+ /*
+ protected function getCleanMessageStacks($aoExtsources, $sType=self::ALL_TAB)
+ {
+ $aoExtsources[] = $this;
+ $aoMessages = array();
+ foreach($aoExtsources as $oExtSource)
+ {
+ $oMessages = $oExtSource->getCleanMessageStack($sType);
+ if($oMessages!='')
+ {
+ $aoMessages[get_class($oExtSource)] = $oMessages;
+ }
+ }
+ return $this->glueMessages($aoMessages);
+ }
+ */
+
+ private function glueMessages($asMessages)
+ {
+ switch($this->iExtractMode)
+ {
+ case self::MODE_TEXT:
+ $oMessageStack = self::recursiveImplode("\n", $asMessages);
+ break;
+ case self::MODE_HTML:
+ $oMessageStack = self::recursiveImplode('
', $asMessages);
+ break;
+ case self::MODE_ARRAY:
+ $oMessageStack = $asMessages;
+ break;
+ case self::MODE_FILE:
+ $oMessageStack = self::recursiveImplode("\n", $asMessages);
+ break;
+ }
+ return $oMessageStack;
+ }
+
+ private static function flattenMessageStack($asTab)
+ {
+ $asFlatTab = array();
+ foreach($asTab as $asRow) {
+ $asFlatTab[] = '['.$asRow['time'].'] '.$asRow['type'].' - '.$asRow['location'].' - '.$asRow['msg'];
+ }
+ return $asFlatTab;
+ }
+
+ private static function recursiveImplode($sGlue, $asTab)
+ {
+ $asTab = self::flattenMessageStack($asTab);
+ return implode($sGlue, $asTab);
+ }
+
+ function __destruct()
+ {
+ $this->cleanMessageStack();
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/inc/Toolbox.php b/inc/Toolbox.php
new file mode 100644
index 0000000..cccd8ff
--- /dev/null
+++ b/inc/Toolbox.php
@@ -0,0 +1,768 @@
+0)
+ {
+ $asCleaningFunc = array_fill(1, count($oData), $sCleaningFunc);
+ $asKeys = array_map(array('self', 'cleanData'), array_keys($oData), $asCleaningFunc);
+ $asValues = array_map(array('self', 'cleanData'), $oData, $asCleaningFunc);
+ return array_combine($asKeys, $asValues);
+ }
+ }
+
+ public static function fixGlobalVars($argv)
+ {
+ //Add CLI arguments
+ if(defined('STDIN')) mb_parse_str(implode('&', array_slice($argv, 1)), $_GET);
+ $_REQUEST = array_merge($_GET, $_REQUEST);
+ }
+
+ public static function array_map_encapsulate($oData, $sChar)
+ {
+ if(is_array($oData))
+ {
+ $asChar = array_fill(1, count($oData), $sChar);
+ return array_combine(array_keys($oData), array_map(array('self', 'array_map_encapsulate'), $oData, $asChar));
+ }
+ else
+ {
+ return $sChar.$oData.$sChar;
+ }
+ }
+
+ public static function capitalizeWords($acText, $sCharList = '')
+ {
+ //Use ucwords if no delimiters are given
+ if($sCharList=='')
+ {
+ return Toolbox::mb_ucwords($acText);
+ }
+
+ // Go through all characters
+ $capitalizeNext = true;
+ $max = mb_strlen($acText);
+ for ($i = 0; $i < $max; $i++)
+ {
+ if(mb_strpos($sCharList, $acText[$i]) !== false)
+ {
+ $capitalizeNext = true;
+ }
+ elseif($capitalizeNext)
+ {
+ $capitalizeNext = false;
+ $acText[$i] = mb_strtoupper($acText[$i]);
+ }
+ }
+
+ return $acText;
+ }
+
+ public static function jsonExport($asData)
+ {
+ header('Content-type: application/json');
+ //return htmlspecialchars(json_encode($asData), ENT_NOQUOTES);
+ return self::jsonConvert($asData);
+ }
+
+ public static function jsonConvert($asData)
+ {
+ return json_encode($asData);
+ }
+
+ public static function curl($sUrl, $bHeader=false, $asPostData=array(), $sReturnType='text', $sCookie='', $sCreds='') {
+ $oCurl = curl_init();
+ curl_setopt($oCurl, CURLOPT_URL, $sUrl);
+ curl_setopt($oCurl, CURLOPT_VERBOSE, false);
+ curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, false);
+
+ curl_setopt($oCurl, CURLOPT_HEADER, $bHeader);
+ if($bHeader) curl_setopt($oCurl, CURLOPT_FOLLOWLOCATION, true);
+
+ if(!empty($asPostData)) {
+ curl_setopt($oCurl, CURLOPT_POST, 1);
+ curl_setopt($oCurl, CURLOPT_POSTFIELDS, $asPostData);
+ }
+
+ curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($oCurl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
+
+ if($sCookie!='') curl_setopt($oCurl, CURLOPT_COOKIE, $sCookie);
+ if($sCreds!='') curl_setopt($oCurl, CURLOPT_USERPWD, $sCreds);
+
+ $sContent = curl_exec($oCurl);
+
+ $sCurlErrorId = curl_errno($oCurl);
+ $sHttpCode = curl_getinfo($oCurl, CURLINFO_HTTP_CODE);
+ $bSuccess = ($sCurlErrorId==0 && $sHttpCode==200);
+ $sDesc = '';
+ if(!$bSuccess) $sDesc = ($sCurlErrorId==0)?('HTTP Error Code '.$sHttpCode):($sCurlErrorId.': '.curl_strerror($sCurlErrorId));
+ curl_close($oCurl);
+
+ switch($sReturnType) {
+ case 'json': $oContent = json_decode($sContent, true); break;
+ default: $oContent = $sContent;
+ }
+
+ return array('result'=>$bSuccess, 'desc'=>$sDesc, 'content'=>$oContent);
+ }
+
+ public static function getMimeType($sPath, $bSubTypeOnly=false)
+ {
+ $sMimetype = '';
+ //Retrieving file Content Type
+ if(file_exists($sPath)) //Local
+ {
+ $oFileInfo = new \finfo(FILEINFO_MIME);
+ $sMimetype = $oFileInfo->file($sPath);
+ }
+ else //Remote
+ {
+ //get_headers($sUrl, 1)
+ $oCurl = curl_init($sPath);
+ curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($oCurl, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($oCurl, CURLOPT_HEADER, true);
+ //curl_setopt($oCurl, CURLOPT_NOBODY, true);
+ curl_setopt($oCurl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
+ curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, false);
+ curl_exec($oCurl);
+ $sMimetype = curl_getinfo($oCurl, CURLINFO_CONTENT_TYPE);
+ curl_close($oCurl);
+ }
+
+ //Only sub type (after /)
+ if($bSubTypeOnly)
+ {
+ preg_match('`\/(?P\w+)(\;|$)`ui', mb_strtolower($sMimetype), $asMatch);
+ if(array_key_exists('type', $asMatch)) $sMimetype = $asMatch['type'];
+ }
+ return $sMimetype;
+ }
+
+ public static function isAnimatedGif($sFilePath)
+ {
+ if(!($oFile = @fopen($sFilePath, 'rb'))) return false;
+
+ $iCount = 0;
+ while (!feof($oFile) && $iCount < 2)
+ {
+
+ $sChunk = fread($oFile, 1024 * 100); //read 100kb at a time
+ $iCount += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $sChunk, $asMatches);
+ }
+ fclose($oFile);
+
+ return $iCount > 1;
+ }
+
+ public static function createThumbnail($sInPath, $iMaxWidth, $iMaxHeight=0, $sOutPath='', $bDeleteIn=false, $asImageExts=array('jpg', 'jpeg', 'gif', 'png'), $bCrop=false, $bCopyExif=false)
+ {
+ $asResult = array('error'=>'');
+
+ //Look up the file type to choose the image creator
+ $sImageExt = self::getMimeType($sInPath, true);
+ $sImageExt = ($sImageExt=='jpg')?'jpeg':$sImageExt;
+
+ //New Destination folder
+ $asInInfo = pathinfo($sInPath);
+ $asOutInfo = pathinfo($sOutPath);
+ if($sOutPath=='') $sOutPath = $sInPath;
+ elseif(mb_substr($sOutPath, -1)=='/') $sOutPath .= mb_strtolower($asInInfo['basename']); //folder only, keep original name
+ elseif(!array_key_exists('extension', $asOutInfo)) $sOutPath .= '.'.$sImageExt; //folder + filename, but getting ext from file info
+
+ //New sizes
+ if(!in_array($sImageExt, $asImageExts) && !empty($asImageExts)) $asResult['error'] = 'Wrong file type: '.$sImageExt;
+ elseif($iMaxWidth==0 && $iMaxHeight==0) $asResult['error'] = 'At least one dimension must be resized (width and/or height)';
+ else
+ {
+ //Recalculate max width/height in case of rotated picture
+ $sRotate = 0;
+ $asExif = @exif_read_data($sInPath);
+ if($asExif && array_key_exists('Orientation', $asExif)) {
+ switch($asExif['Orientation'])
+ {
+ case 3: $sRotate = 180; break; //Flip over
+ case 6: $sRotate = -90; break; //Clockwise
+ case 8: $sRotate = 90; break; //Trigo
+ }
+ }
+
+ list($iWidth, $iHeight) = getimagesize($sInPath);
+ if(abs($sRotate) == 90) {
+ $iTempWidth = $iWidth;
+ $iWidth = $iHeight;
+ $iHeight = $iTempWidth;
+ }
+
+ //Limit on only 1 parameter
+ if($iMaxWidth==0) $iMaxWidth = $iWidth * $iMaxHeight / $iHeight;
+ elseif($iMaxHeight==0) $iMaxHeight = $iHeight * $iMaxWidth / $iWidth;
+
+ if($iWidth > $iMaxWidth || $iHeight > $iMaxHeight)
+ {
+ $dResizeDeltaWidth = $iWidth - $iMaxWidth;
+ $dResizeDeltaHeight = $iHeight - $iMaxHeight;
+ $iPosLeft = $iPosTop = 0;
+ $iThumbWidth = $iMaxWidth;
+ $iThumbHeight = $iMaxHeight;
+
+ //Max up the lowest value between height and width and crop the other
+ if($bCrop)
+ {
+ if($dResizeDeltaWidth > $dResizeDeltaHeight) //Crop width
+ {
+ $iResizedHeight = $iMaxHeight;
+ $iResizedWidth = ($iResizedHeight / $iHeight) * $iWidth;
+ $iPosLeft = ($iResizedWidth - $iMaxWidth)/2*(-1);
+ }
+ else
+ {
+ $iResizedWidth = $iMaxWidth;
+ $iResizedHeight = ($iResizedWidth / $iWidth) * $iHeight;
+ $iPosTop = ($iResizedHeight - $iMaxHeight)/2*(-1);
+ }
+ }
+ else //Just resize
+ {
+ if($dResizeDeltaWidth > $dResizeDeltaHeight)
+ {
+ $iResizedWidth = $iMaxWidth;
+ $iResizedHeight = ($iResizedWidth / $iWidth) * $iHeight;
+ }
+ else
+ {
+ $iResizedHeight = $iMaxHeight;
+ $iResizedWidth = ($iResizedHeight / $iHeight) * $iWidth;
+ }
+ $iThumbWidth = $iResizedWidth;
+ $iThumbHeight = $iResizedHeight;
+ }
+
+ if($sImageExt=='gif' && self::isAnimatedGif($sInPath))
+ {
+ $sContent = file_get_contents($sInPath);
+ $sBigGifPath = uniqid().$sOutPath.'.big';
+ $sCoalescePath = uniqid().$sOutPath.'.coalesce';
+
+ if(file_put_contents($sBigGifPath, $sContent)===false) $asResult['error'] = 'Unable to create temporary thumbnail : '.$sBigGifPath;
+ system('convert '.$sBigGifPath.' -coalesce '.$sCoalescePath);
+ system('convert -size '.$iWidth.'x'.$iHeight.' '.$sCoalescePath.' -resize '.$iResizedWidth.'x'.$iResizedHeight.' '.$sOutPath);
+ unlink($sBigGifPath);
+ unlink($sCoalescePath);
+ }
+ else
+ {
+ //create image from source
+ $oSource = call_user_func('imagecreatefrom'.$sImageExt, $sInPath);
+
+ //Fix rotation
+ if($sRotate) $oSource = imagerotate($oSource, $sRotate, 0);
+
+ //Resize
+ $oThumb = imagecreatetruecolor($iThumbWidth, $iThumbHeight);
+ imagecopyresampled($oThumb, $oSource, $iPosLeft, $iPosTop, 0, 0, $iResizedWidth, $iResizedHeight, $iWidth, $iHeight);
+
+ //Save
+ if(file_exists($sOutPath)) unlink($sOutPath);
+ if(!call_user_func_array('image'.$sImageExt, array($oThumb, $sOutPath)))
+ {
+ $asResult['error'] = 'Unable to create thumbnail : '.$sOutPath;
+ }
+ imagedestroy($oThumb);
+ }
+ }
+ elseif($sInPath != $sOutPath)
+ {
+ $iResizedWidth = $iWidth;
+ $iResizedHeight = $iHeight;
+ if(!copy($sInPath, $sOutPath)) $asResult['error'] = 'Copy failed from '.$sInPath.' to '.$sOutPath;
+ }
+ $asResult['width'] = $iResizedWidth;
+ $asResult['height'] = $iResizedHeight;
+ $asResult['out'] = $sOutPath;
+ }
+
+ if($bCopyExif && $asResult['error'] == '') self::copyExif($sInPath, $sOutPath);
+ if($bDeleteIn && $asResult['error'] == '' && $sInPath != $sOutPath) unlink($sInPath);
+
+ return $asResult;
+ }
+
+ public static function copyExif($srcfile, $destfile) {
+ // Function transfers EXIF (APP1) and IPTC (APP13) from $srcfile and adds it to $destfile
+ // JPEG file has format 0xFFD8 + [APP0] + [APP1] + ... [APP15] + where [APPi] are optional
+ // Segment APPi (where i=0x0 to 0xF) has format 0xFFEi + 0xMM + 0xLL + (where 0xMM is
+ // most significant 8 bits of (strlen() + 2) and 0xLL is the least significant 8 bits
+ // of (strlen() + 2)
+
+ if (file_exists($srcfile) && file_exists($destfile)) {
+ $srcsize = @getimagesize($srcfile, $imageinfo);
+ // Prepare EXIF data bytes from source file
+ $exifdata = (is_array($imageinfo) && key_exists("APP1", $imageinfo)) ? $imageinfo['APP1'] : null;
+ if ($exifdata) {
+ $exiflength = strlen($exifdata) + 2;
+ if ($exiflength > 0xFFFF) return false;
+ // Construct EXIF segment
+ $exifdata = chr(0xFF) . chr(0xE1) . chr(($exiflength >> 8) & 0xFF) . chr($exiflength & 0xFF) . $exifdata;
+ }
+ // Prepare IPTC data bytes from source file
+ $iptcdata = (is_array($imageinfo) && key_exists("APP13", $imageinfo)) ? $imageinfo['APP13'] : null;
+ if ($iptcdata) {
+ $iptclength = strlen($iptcdata) + 2;
+ if ($iptclength > 0xFFFF) return false;
+ // Construct IPTC segment
+ $iptcdata = chr(0xFF) . chr(0xED) . chr(($iptclength >> 8) & 0xFF) . chr($iptclength & 0xFF) . $iptcdata;
+ }
+ $destfilecontent = @file_get_contents($destfile);
+ if (!$destfilecontent) return false;
+ if (strlen($destfilecontent) > 0) {
+ $destfilecontent = substr($destfilecontent, 2);
+ $portiontoadd = chr(0xFF) . chr(0xD8); // Variable accumulates new & original IPTC application segments
+ $exifadded = !$exifdata;
+ $iptcadded = !$iptcdata;
+
+ while ((substr($destfilecontent, 0, 2) & 0xFFF0) === 0xFFE0) {
+ $segmentlen = (substr($destfilecontent, 2, 2) & 0xFFFF);
+ $iptcsegmentnumber = (substr($destfilecontent, 1, 1) & 0x0F); // Last 4 bits of second byte is IPTC segment #
+ if ($segmentlen <= 2) return false;
+ $thisexistingsegment = substr($destfilecontent, 0, $segmentlen + 2);
+ if ((1 <= $iptcsegmentnumber) && (!$exifadded)) {
+ $portiontoadd .= $exifdata;
+ $exifadded = true;
+ if (1 === $iptcsegmentnumber) $thisexistingsegment = '';
+ }
+ if ((13 <= $iptcsegmentnumber) && (!$iptcadded)) {
+ $portiontoadd .= $iptcdata;
+ $iptcadded = true;
+ if (13 === $iptcsegmentnumber) $thisexistingsegment = '';
+ }
+ $portiontoadd .= $thisexistingsegment;
+ $destfilecontent = substr($destfilecontent, $segmentlen + 2);
+ }
+ if (!$exifadded) $portiontoadd .= $exifdata; // Add EXIF data if not added already
+ if (!$iptcadded) $portiontoadd .= $iptcdata; // Add IPTC data if not added already
+ $outputfile = fopen($destfile, 'w');
+ if ($outputfile) return fwrite($outputfile, $portiontoadd . $destfilecontent); else return false;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ public static function utf8_compliant($sText)
+ {
+ if(strlen($sText) == 0) return true;
+ return (preg_match('/^.{1}/us', $sText, $ar) == 1);
+ }
+
+ public static function mb_ucwords($sText)
+ {
+ return mb_convert_case($sText, MB_CASE_TITLE, "UTF-8");
+ }
+
+ public static function mb_ucfirst($sText)
+ {
+ $sLength = mb_strlen($sText);
+ $sFirstChar = mb_substr($sText, 0, 1);
+ $sThen = mb_substr($sText, 1, $sLength - 1);
+ return mb_strtoupper($sFirstChar).$sThen;
+ }
+
+ public static function file_get_contents_utf8($oFile)
+ {
+ $sContent = file_get_contents($oFile);
+ return mb_convert_encoding($sContent, 'UTF-8', mb_detect_encoding($sContent, 'UTF-8, ISO-8859-1', true));
+ }
+
+ public static function rgbToHex($R, $G, $B)
+ {
+ $R = dechex($R);
+ if(strlen($R)<2) $R='0'.$R;
+
+ $G = dechex($G);
+ if(strlen($G)<2) $G='0'.$G;
+
+ $B = dechex($B);
+ if(strlen($B)<2) $B='0'.$B;
+
+ return $R.$G.$B;
+ }
+
+ public static function setCookie($sCookieName, $sCookieValue, $iDays)
+ {
+ $iTimeLimit = time()+60*60*24*$iDays;
+ setcookie($sCookieName, $sCookieValue, $iTimeLimit);
+ }
+
+ public static function fixEOL($sText)
+ {
+ //Normalize line endings
+ //Convert all line-endings to UNIX format
+ $sText = str_replace("\r\n", "\n", $sText); //Windows
+ $sText = str_replace("\r", "\n", $sText); //Mac
+
+ // Don't allow out-of-control blank lines
+ $sText = preg_replace("/\n{2,}/", "\n\n", $sText);
+ return $sText;
+ }
+
+ public static function getUserLanguage($available_languages, $sDefaultLang='') {
+ $http_accept_language = isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])?$_SERVER['HTTP_ACCEPT_LANGUAGE']:'';
+
+ //Format: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
+ preg_match_all("/([[:alpha:]]{1,8})(-([[:alpha:]|-]{1,8}))?(\s*;\s*q\s*=\s*(1\.0{0,3}|0\.\d{0,3}))?\s*(,|$)/i", $http_accept_language, $hits, PREG_SET_ORDER);
+
+ $bestlang = $sDefaultLang;
+ $bestqval = 0;
+
+ foreach($hits as $arr) {
+ $langprefix = strtolower($arr[1]);
+ if(!empty($arr[3])) {
+ $langrange = strtolower($arr[3]);
+ $language = $langprefix.'-'.$langrange;
+ }
+ else $language = $langprefix;
+
+ //Q Value
+ $qvalue = 1.0;
+ if(!empty($arr[5])) $qvalue = floatval($arr[5]);
+
+ //find q-maximal language
+ if(in_array($language, $available_languages) && $qvalue > $bestqval) {
+ $bestlang = $language;
+ $bestqval = $qvalue;
+ }
+ //if no direct hit, try the prefix only but decrease q-value by 10% (as http_negotiate_language does)
+ elseif(in_array($langprefix, $available_languages) && ($qvalue * 0.9) > $bestqval) {
+ $bestlang = $langprefix;
+ $bestqval = $qvalue * 0.9;
+ }
+ }
+ return $bestlang;
+ }
+
+ /**
+ * Return relative time description
+ * FIXME shitty implementation of i18n
+ * @param int $oTime Time (strtotime)
+ * @param string $sLang Language (en/fr)
+ * @return string Relative Time
+ */
+ public static function getDateTimeDesc($oTime, $sLang='en')
+ {
+ $iTimeStamp = is_numeric($oTime)?$oTime:strtotime($oTime);
+ $sCurTimeStamp = time();
+
+ switch ($sLang) {
+ case 'en':
+ $asWeekDays = array('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'satursday', 'sunday');
+ $asMonths = array('january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december');
+ break;
+ case 'fr':
+ $asWeekDays = array('lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche');
+ $asMonths = array('janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre');
+ break;
+ }
+
+ $sSep = '|';
+ $sFormat = 'Y'.$sSep.'n'.$sSep.'W'.$sSep.'N'.$sSep.'j'.$sSep.'G';
+ list($sYear, $sMonth, $sWeek, $sWeekDay, $sDay, $sHour) = explode($sSep, date($sFormat, $iTimeStamp));
+ list($sCurYear, $sCurMonth, $sCurWeek, $sCurWeekDay, $sCurDay, $sCurHour) = explode($sSep, date($sFormat, $sCurTimeStamp));
+
+ $sDesc = '';
+ switch ($sLang) {
+ case 'en':
+ if($iTimeStamp>$sCurTimeStamp) $sDesc = 'in the future';
+ elseif($sCurTimeStamp-$iTimeStamp<60) $sDesc = 'a few seconds ago';
+ elseif($sCurTimeStamp-$iTimeStamp<60*10) $sDesc = 'a few minutes ago';
+ elseif($sCurTimeStamp-$iTimeStamp<60*20) $sDesc = '15 minutes ago';
+ elseif($sCurTimeStamp-$iTimeStamp<60*50) $sDesc = 'half an hour ago';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*2) $sDesc = 'an hour ago';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24 && $sDay==$sCurDay) $sDesc = 'at '.date('gA', $iTimeStamp);
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24) $sDesc = 'yesterday';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*7 && $sWeek==$sCurWeek) $sDesc = $asWeekDays[$sWeekDay-1];
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*7) $sDesc = 'last '.$asWeekDays[$sWeekDay-1];
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*9) $sDesc = 'a week ago';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*12) $sDesc = '10 days ago';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*16) $sDesc = '2 weeks ago';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*23) $sDesc = '3 weeks ago';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*31 && $sMonth==$sCurMonth) $sDesc = 'on '.$asMonths[$sMonth-1].', '.$sDay;
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*30*2 && $sMonth==($sCurMonth-1)) $sDesc = 'last month';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*365 && $sYear==$sCurYear) $sDesc = 'in '.$asMonths[$sMonth-1];
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*365) $sDesc = 'in '.$asMonths[$sMonth-1].' '.$sYear;
+ elseif($sYear==($sCurYear-1)) $sDesc = 'last year';
+ else $sDesc = 'in '.$sYear;
+ break;
+ case 'fr':
+ if($iTimeStamp>$sCurTimeStamp) $sDesc = 'dans le futur';
+ elseif($sCurTimeStamp-$iTimeStamp<60) $sDesc = 'il y a quelques secondes';
+ elseif($sCurTimeStamp-$iTimeStamp<60*10) $sDesc = 'il y a quelques minutes';
+ elseif($sCurTimeStamp-$iTimeStamp<60*20) $sDesc = 'il y a un quart d\'heure';
+ elseif($sCurTimeStamp-$iTimeStamp<60*50) $sDesc = 'il y a une demi-heure';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*2) $sDesc = 'il y a une heure';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24 && $sDay==$sCurDay) $sDesc = 'à '.$sHour.'h';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24) $sDesc = 'hier';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*7 && $sWeek==$sCurWeek) $sDesc = $asWeekDays[$sWeekDay-1];
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*7) $sDesc = $asWeekDays[$sWeekDay-1].' dernier';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*9) $sDesc = 'il y a une semaine';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*12) $sDesc = 'il y a 10 jours';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*16) $sDesc = 'il y a 2 semaines';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*23) $sDesc = 'il y a 3 semaines';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*31 && $sMonth==$sCurMonth) $sDesc = 'le '.$sDay.' '.$asMonths[$sMonth-1];
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*30*2 && $sMonth==($sCurMonth-1)) $sDesc = 'le mois dernier';
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*365 && $sYear==$sCurYear) $sDesc = 'en '.$asMonths[$sMonth-1];
+ elseif($sCurTimeStamp-$iTimeStamp<60*60*24*365) $sDesc = 'en '.$asMonths[$sMonth-1].' '.$sYear;
+ elseif($sYear==($sCurYear-1)) $sDesc = 'l\'année dernière';
+ else $sDesc = 'en '.$sYear;
+ break;
+ }
+
+ //return self::mb_ucfirst($sDesc);
+ return $sDesc;
+ }
+
+ /**
+ * Worpress function to remove accents
+ * https://core.trac.wordpress.org/browser/trunk/src/wp-includes/formatting.php
+ * @param String $string
+ * @return String
+ */
+ public static function remove_accents($string)
+ {
+ if(!preg_match('/[\x80-\xff]/', $string)) return $string;
+
+ $chars = array(
+ // Decompositions for Latin-1 Supplement
+ chr(194).chr(170) => 'a', chr(194).chr(186) => 'o',
+ chr(195).chr(128) => 'A', chr(195).chr(129) => 'A',
+ chr(195).chr(130) => 'A', chr(195).chr(131) => 'A',
+ chr(195).chr(132) => 'A', chr(195).chr(133) => 'A',
+ chr(195).chr(134) => 'AE',chr(195).chr(135) => 'C',
+ chr(195).chr(136) => 'E', chr(195).chr(137) => 'E',
+ chr(195).chr(138) => 'E', chr(195).chr(139) => 'E',
+ chr(195).chr(140) => 'I', chr(195).chr(141) => 'I',
+ chr(195).chr(142) => 'I', chr(195).chr(143) => 'I',
+ chr(195).chr(144) => 'D', chr(195).chr(145) => 'N',
+ chr(195).chr(146) => 'O', chr(195).chr(147) => 'O',
+ chr(195).chr(148) => 'O', chr(195).chr(149) => 'O',
+ chr(195).chr(150) => 'O', chr(195).chr(153) => 'U',
+ chr(195).chr(154) => 'U', chr(195).chr(155) => 'U',
+ chr(195).chr(156) => 'U', chr(195).chr(157) => 'Y',
+ chr(195).chr(158) => 'TH',chr(195).chr(159) => 's',
+ chr(195).chr(160) => 'a', chr(195).chr(161) => 'a',
+ chr(195).chr(162) => 'a', chr(195).chr(163) => 'a',
+ chr(195).chr(164) => 'a', chr(195).chr(165) => 'a',
+ chr(195).chr(166) => 'ae',chr(195).chr(167) => 'c',
+ chr(195).chr(168) => 'e', chr(195).chr(169) => 'e',
+ chr(195).chr(170) => 'e', chr(195).chr(171) => 'e',
+ chr(195).chr(172) => 'i', chr(195).chr(173) => 'i',
+ chr(195).chr(174) => 'i', chr(195).chr(175) => 'i',
+ chr(195).chr(176) => 'd', chr(195).chr(177) => 'n',
+ chr(195).chr(178) => 'o', chr(195).chr(179) => 'o',
+ chr(195).chr(180) => 'o', chr(195).chr(181) => 'o',
+ chr(195).chr(182) => 'o', chr(195).chr(184) => 'o',
+ chr(195).chr(185) => 'u', chr(195).chr(186) => 'u',
+ chr(195).chr(187) => 'u', chr(195).chr(188) => 'u',
+ chr(195).chr(189) => 'y', chr(195).chr(190) => 'th',
+ chr(195).chr(191) => 'y', chr(195).chr(152) => 'O',
+ // Decompositions for Latin Extended-A
+ chr(196).chr(128) => 'A', chr(196).chr(129) => 'a',
+ chr(196).chr(130) => 'A', chr(196).chr(131) => 'a',
+ chr(196).chr(132) => 'A', chr(196).chr(133) => 'a',
+ chr(196).chr(134) => 'C', chr(196).chr(135) => 'c',
+ chr(196).chr(136) => 'C', chr(196).chr(137) => 'c',
+ chr(196).chr(138) => 'C', chr(196).chr(139) => 'c',
+ chr(196).chr(140) => 'C', chr(196).chr(141) => 'c',
+ chr(196).chr(142) => 'D', chr(196).chr(143) => 'd',
+ chr(196).chr(144) => 'D', chr(196).chr(145) => 'd',
+ chr(196).chr(146) => 'E', chr(196).chr(147) => 'e',
+ chr(196).chr(148) => 'E', chr(196).chr(149) => 'e',
+ chr(196).chr(150) => 'E', chr(196).chr(151) => 'e',
+ chr(196).chr(152) => 'E', chr(196).chr(153) => 'e',
+ chr(196).chr(154) => 'E', chr(196).chr(155) => 'e',
+ chr(196).chr(156) => 'G', chr(196).chr(157) => 'g',
+ chr(196).chr(158) => 'G', chr(196).chr(159) => 'g',
+ chr(196).chr(160) => 'G', chr(196).chr(161) => 'g',
+ chr(196).chr(162) => 'G', chr(196).chr(163) => 'g',
+ chr(196).chr(164) => 'H', chr(196).chr(165) => 'h',
+ chr(196).chr(166) => 'H', chr(196).chr(167) => 'h',
+ chr(196).chr(168) => 'I', chr(196).chr(169) => 'i',
+ chr(196).chr(170) => 'I', chr(196).chr(171) => 'i',
+ chr(196).chr(172) => 'I', chr(196).chr(173) => 'i',
+ chr(196).chr(174) => 'I', chr(196).chr(175) => 'i',
+ chr(196).chr(176) => 'I', chr(196).chr(177) => 'i',
+ chr(196).chr(178) => 'IJ',chr(196).chr(179) => 'ij',
+ chr(196).chr(180) => 'J', chr(196).chr(181) => 'j',
+ chr(196).chr(182) => 'K', chr(196).chr(183) => 'k',
+ chr(196).chr(184) => 'k', chr(196).chr(185) => 'L',
+ chr(196).chr(186) => 'l', chr(196).chr(187) => 'L',
+ chr(196).chr(188) => 'l', chr(196).chr(189) => 'L',
+ chr(196).chr(190) => 'l', chr(196).chr(191) => 'L',
+ chr(197).chr(128) => 'l', chr(197).chr(129) => 'L',
+ chr(197).chr(130) => 'l', chr(197).chr(131) => 'N',
+ chr(197).chr(132) => 'n', chr(197).chr(133) => 'N',
+ chr(197).chr(134) => 'n', chr(197).chr(135) => 'N',
+ chr(197).chr(136) => 'n', chr(197).chr(137) => 'N',
+ chr(197).chr(138) => 'n', chr(197).chr(139) => 'N',
+ chr(197).chr(140) => 'O', chr(197).chr(141) => 'o',
+ chr(197).chr(142) => 'O', chr(197).chr(143) => 'o',
+ chr(197).chr(144) => 'O', chr(197).chr(145) => 'o',
+ chr(197).chr(146) => 'OE',chr(197).chr(147) => 'oe',
+ chr(197).chr(148) => 'R',chr(197).chr(149) => 'r',
+ chr(197).chr(150) => 'R',chr(197).chr(151) => 'r',
+ chr(197).chr(152) => 'R',chr(197).chr(153) => 'r',
+ chr(197).chr(154) => 'S',chr(197).chr(155) => 's',
+ chr(197).chr(156) => 'S',chr(197).chr(157) => 's',
+ chr(197).chr(158) => 'S',chr(197).chr(159) => 's',
+ chr(197).chr(160) => 'S', chr(197).chr(161) => 's',
+ chr(197).chr(162) => 'T', chr(197).chr(163) => 't',
+ chr(197).chr(164) => 'T', chr(197).chr(165) => 't',
+ chr(197).chr(166) => 'T', chr(197).chr(167) => 't',
+ chr(197).chr(168) => 'U', chr(197).chr(169) => 'u',
+ chr(197).chr(170) => 'U', chr(197).chr(171) => 'u',
+ chr(197).chr(172) => 'U', chr(197).chr(173) => 'u',
+ chr(197).chr(174) => 'U', chr(197).chr(175) => 'u',
+ chr(197).chr(176) => 'U', chr(197).chr(177) => 'u',
+ chr(197).chr(178) => 'U', chr(197).chr(179) => 'u',
+ chr(197).chr(180) => 'W', chr(197).chr(181) => 'w',
+ chr(197).chr(182) => 'Y', chr(197).chr(183) => 'y',
+ chr(197).chr(184) => 'Y', chr(197).chr(185) => 'Z',
+ chr(197).chr(186) => 'z', chr(197).chr(187) => 'Z',
+ chr(197).chr(188) => 'z', chr(197).chr(189) => 'Z',
+ chr(197).chr(190) => 'z', chr(197).chr(191) => 's',
+ // Decompositions for Latin Extended-B
+ chr(200).chr(152) => 'S', chr(200).chr(153) => 's',
+ chr(200).chr(154) => 'T', chr(200).chr(155) => 't',
+ // Euro Sign
+ chr(226).chr(130).chr(172) => 'E',
+ // GBP (Pound) Sign
+ chr(194).chr(163) => '',
+ // Vowels with diacritic (Vietnamese)
+ // unmarked
+ chr(198).chr(160) => 'O', chr(198).chr(161) => 'o',
+ chr(198).chr(175) => 'U', chr(198).chr(176) => 'u',
+ // grave accent
+ chr(225).chr(186).chr(166) => 'A', chr(225).chr(186).chr(167) => 'a',
+ chr(225).chr(186).chr(176) => 'A', chr(225).chr(186).chr(177) => 'a',
+ chr(225).chr(187).chr(128) => 'E', chr(225).chr(187).chr(129) => 'e',
+ chr(225).chr(187).chr(146) => 'O', chr(225).chr(187).chr(147) => 'o',
+ chr(225).chr(187).chr(156) => 'O', chr(225).chr(187).chr(157) => 'o',
+ chr(225).chr(187).chr(170) => 'U', chr(225).chr(187).chr(171) => 'u',
+ chr(225).chr(187).chr(178) => 'Y', chr(225).chr(187).chr(179) => 'y',
+ // hook
+ chr(225).chr(186).chr(162) => 'A', chr(225).chr(186).chr(163) => 'a',
+ chr(225).chr(186).chr(168) => 'A', chr(225).chr(186).chr(169) => 'a',
+ chr(225).chr(186).chr(178) => 'A', chr(225).chr(186).chr(179) => 'a',
+ chr(225).chr(186).chr(186) => 'E', chr(225).chr(186).chr(187) => 'e',
+ chr(225).chr(187).chr(130) => 'E', chr(225).chr(187).chr(131) => 'e',
+ chr(225).chr(187).chr(136) => 'I', chr(225).chr(187).chr(137) => 'i',
+ chr(225).chr(187).chr(142) => 'O', chr(225).chr(187).chr(143) => 'o',
+ chr(225).chr(187).chr(148) => 'O', chr(225).chr(187).chr(149) => 'o',
+ chr(225).chr(187).chr(158) => 'O', chr(225).chr(187).chr(159) => 'o',
+ chr(225).chr(187).chr(166) => 'U', chr(225).chr(187).chr(167) => 'u',
+ chr(225).chr(187).chr(172) => 'U', chr(225).chr(187).chr(173) => 'u',
+ chr(225).chr(187).chr(182) => 'Y', chr(225).chr(187).chr(183) => 'y',
+ // tilde
+ chr(225).chr(186).chr(170) => 'A', chr(225).chr(186).chr(171) => 'a',
+ chr(225).chr(186).chr(180) => 'A', chr(225).chr(186).chr(181) => 'a',
+ chr(225).chr(186).chr(188) => 'E', chr(225).chr(186).chr(189) => 'e',
+ chr(225).chr(187).chr(132) => 'E', chr(225).chr(187).chr(133) => 'e',
+ chr(225).chr(187).chr(150) => 'O', chr(225).chr(187).chr(151) => 'o',
+ chr(225).chr(187).chr(160) => 'O', chr(225).chr(187).chr(161) => 'o',
+ chr(225).chr(187).chr(174) => 'U', chr(225).chr(187).chr(175) => 'u',
+ chr(225).chr(187).chr(184) => 'Y', chr(225).chr(187).chr(185) => 'y',
+ // acute accent
+ chr(225).chr(186).chr(164) => 'A', chr(225).chr(186).chr(165) => 'a',
+ chr(225).chr(186).chr(174) => 'A', chr(225).chr(186).chr(175) => 'a',
+ chr(225).chr(186).chr(190) => 'E', chr(225).chr(186).chr(191) => 'e',
+ chr(225).chr(187).chr(144) => 'O', chr(225).chr(187).chr(145) => 'o',
+ chr(225).chr(187).chr(154) => 'O', chr(225).chr(187).chr(155) => 'o',
+ chr(225).chr(187).chr(168) => 'U', chr(225).chr(187).chr(169) => 'u',
+ // dot below
+ chr(225).chr(186).chr(160) => 'A', chr(225).chr(186).chr(161) => 'a',
+ chr(225).chr(186).chr(172) => 'A', chr(225).chr(186).chr(173) => 'a',
+ chr(225).chr(186).chr(182) => 'A', chr(225).chr(186).chr(183) => 'a',
+ chr(225).chr(186).chr(184) => 'E', chr(225).chr(186).chr(185) => 'e',
+ chr(225).chr(187).chr(134) => 'E', chr(225).chr(187).chr(135) => 'e',
+ chr(225).chr(187).chr(138) => 'I', chr(225).chr(187).chr(139) => 'i',
+ chr(225).chr(187).chr(140) => 'O', chr(225).chr(187).chr(141) => 'o',
+ chr(225).chr(187).chr(152) => 'O', chr(225).chr(187).chr(153) => 'o',
+ chr(225).chr(187).chr(162) => 'O', chr(225).chr(187).chr(163) => 'o',
+ chr(225).chr(187).chr(164) => 'U', chr(225).chr(187).chr(165) => 'u',
+ chr(225).chr(187).chr(176) => 'U', chr(225).chr(187).chr(177) => 'u',
+ chr(225).chr(187).chr(180) => 'Y', chr(225).chr(187).chr(181) => 'y',
+ // Vowels with diacritic (Chinese, Hanyu Pinyin)
+ chr(201).chr(145) => 'a',
+ // macron
+ chr(199).chr(149) => 'U', chr(199).chr(150) => 'u',
+ // acute accent
+ chr(199).chr(151) => 'U', chr(199).chr(152) => 'u',
+ // caron
+ chr(199).chr(141) => 'A', chr(199).chr(142) => 'a',
+ chr(199).chr(143) => 'I', chr(199).chr(144) => 'i',
+ chr(199).chr(145) => 'O', chr(199).chr(146) => 'o',
+ chr(199).chr(147) => 'U', chr(199).chr(148) => 'u',
+ chr(199).chr(153) => 'U', chr(199).chr(154) => 'u',
+ // grave accent
+ chr(199).chr(155) => 'U', chr(199).chr(156) => 'u',
+ );
+
+ /*
+ // Used for locale-specific rules
+ $locale = get_locale();
+
+ if ( 'de_DE' == $locale ) {
+ $chars[ chr(195).chr(132) ] = 'Ae';
+ $chars[ chr(195).chr(164) ] = 'ae';
+ $chars[ chr(195).chr(150) ] = 'Oe';
+ $chars[ chr(195).chr(182) ] = 'oe';
+ $chars[ chr(195).chr(156) ] = 'Ue';
+ $chars[ chr(195).chr(188) ] = 'ue';
+ $chars[ chr(195).chr(159) ] = 'ss';
+ } elseif ( 'da_DK' === $locale ) {
+ $chars[ chr(195).chr(134) ] = 'Ae';
+ $chars[ chr(195).chr(166) ] = 'ae';
+ $chars[ chr(195).chr(152) ] = 'Oe';
+ $chars[ chr(195).chr(184) ] = 'oe';
+ $chars[ chr(195).chr(133) ] = 'Aa';
+ $chars[ chr(195).chr(165) ] = 'aa';
+ }
+ */
+ $string = strtr($string, $chars);
+
+
+ return $string;
+ }
+}
+
+?>
diff --git a/inc/Translator.php b/inc/Translator.php
new file mode 100644
index 0000000..c85a76c
--- /dev/null
+++ b/inc/Translator.php
@@ -0,0 +1,134 @@
+asLanguages = array();
+ $this->asTranslations = array();
+ $this->loadLanguages();
+ $this->setLanguage($sLang, $sDefaultLang);
+ }
+
+ public function setLanguage($sLang, $sDefaultLang='') {
+ $this->sDefaultLang = ($sDefaultLang=='')?self::DEFAULT_LANG:$sDefaultLang;
+ $this->sLang = ($sLang=='')?ToolBox::getUserLanguage($this->asLanguages, $this->sDefaultLang):$sLang;
+ }
+
+ public function getLanguage() {
+ return $this->sLang;
+ }
+
+ public function getTranslation($sTransKey='', $asParams=array(), $sLang='')
+ {
+ $oTransText = false;
+
+ //Select language & Scope
+ if($sLang=='') $sLang = $this->sLang;
+ $bAllTrans = ($sTransKey=='');
+
+ //Look up in the selected language dictionary
+ if(in_array($sLang, $this->asLanguages) && ($bAllTrans || array_key_exists($sTransKey, $this->asTranslations[$sLang]))) {
+ if($bAllTrans) $oTransText = array_merge($this->asTranslations[$this->sDefaultLang], $this->asTranslations[$sLang]);
+ else $oTransText = $this->asTranslations[$sLang][$sTransKey];
+ }
+ //Look up in the default language dictionary
+ elseif(!$bAllTrans && array_key_exists($sTransKey, $this->asTranslations[$this->sDefaultLang])) {
+ $this->addWarning('Missing translation in "'.$sLang.'" for the key "'.$sTransKey.'", falling back to "'.$this->sDefaultLang.'"');
+ $oTransText = $this->asTranslations[$this->sDefaultLang][$sTransKey];
+ }
+ else $this->addWarning($bAllTrans?'Missing language "'.$sLang.'"':'Missing translation in "'.$sLang.'" for the key "'.$sTransKey.'"');
+
+ if(is_string($asParams)) $asParams = array($asParams);
+ if($oTransText && !$bAllTrans && !empty($asParams)) {
+ foreach($asParams as $iIndex=>$sParam) $oTransText = str_replace('$'.$iIndex, $sParam, $oTransText);
+ }
+
+ return $oTransText;
+ }
+
+ public function getTranslations($sLang='') {
+ return $this->getTranslation('', $sLang);
+ }
+
+ public function getHashToPage($asMenuPages)
+ {
+ $asHashToPage = array();
+ foreach($asMenuPages as $sHash)
+ {
+ foreach($this->asLanguages as $sLang)
+ {
+ $asHashToPage[$this->getTranslation('menu_'.$sHash.'_key', $sLang)] = $sHash;
+ }
+ }
+ return $asHashToPage;
+ }
+
+ public function getPageToHash($asMenuPages)
+ {
+ $asPageToHash = array();
+ foreach($asMenuPages as $sHash)
+ {
+ $asPageToHash[$sHash] = $this->getTranslation('menu_'.$sHash.'_key');
+ }
+ return $asPageToHash;
+ }
+
+ private function loadLanguages()
+ {
+ //List all available languages
+ $asLangPaths = glob(self::getLangPath('*'));
+ $this->asLanguages = array_map('basename', $asLangPaths, array_fill(1, count($asLangPaths), self::LANG_EXT));
+
+ //Load languages
+ array_walk($this->asLanguages, array($this, 'loadLanguageFile'));
+ }
+
+ private function loadLanguageFile($sLang)
+ {
+ if(!in_array($sLang, $this->asTranslations))
+ {
+ $sData = file_get_contents(self::getLangPath($sLang));
+ $asData = explode("\n", $sData);
+ foreach($asData as $sTranslation)
+ {
+ $iSepPos = mb_stripos($sTranslation, self::LANG_SEP);
+ if($iSepPos !== false)
+ {
+ $sTransKey = trim(mb_substr($sTranslation, 0, $iSepPos));
+ $sTransText = trim(mb_substr($sTranslation, $iSepPos+1));
+ $this->asTranslations[$sLang][$sTransKey] = $sTransText;
+ }
+ }
+ }
+ }
+
+ private static function getLangPath($sLang)
+ {
+ return self::LANG_FOLDER.$sLang.self::LANG_EXT;
+ }
+}
\ No newline at end of file
diff --git a/inc/UploadHandler.php b/inc/UploadHandler.php
new file mode 100644
index 0000000..3584a80
--- /dev/null
+++ b/inc/UploadHandler.php
@@ -0,0 +1,1483 @@
+ 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
+ 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
+ 3 => 'The uploaded file was only partially uploaded',
+ 4 => 'No file was uploaded',
+ 6 => 'Missing a temporary folder',
+ 7 => 'Failed to write file to disk',
+ 8 => 'A PHP extension stopped the file upload',
+ 'post_max_size' => 'The uploaded file exceeds the post_max_size directive in php.ini',
+ 'max_file_size' => 'File is too big',
+ 'min_file_size' => 'File is too small',
+ 'accept_file_types' => 'Filetype not allowed',
+ 'max_number_of_files' => 'Maximum number of files exceeded',
+ 'invalid_file_type' => 'Invalid file type',
+ 'max_width' => 'Image exceeds maximum width',
+ 'min_width' => 'Image requires a minimum width',
+ 'max_height' => 'Image exceeds maximum height',
+ 'min_height' => 'Image requires a minimum height',
+ 'abort' => 'File upload aborted',
+ 'image_resize' => 'Failed to resize image'
+ );
+
+ const IMAGETYPE_GIF = 'image/gif';
+ const IMAGETYPE_JPEG = 'image/jpeg';
+ const IMAGETYPE_PNG = 'image/png';
+
+ protected $image_objects = array();
+ protected $response = array();
+
+ public function __construct($options = null, $initialize = true, $error_messages = null) {
+ $this->options = array(
+ 'script_url' => $this->get_full_url().'/'.$this->basename($this->get_server_var('SCRIPT_NAME')),
+ 'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/files/',
+ 'upload_url' => $this->get_full_url().'/files/',
+ 'input_stream' => 'php://input',
+ 'user_dirs' => false,
+ 'mkdir_mode' => 0755,
+ 'param_name' => 'files',
+ // Set the following option to 'POST', if your server does not support
+ // DELETE requests. This is a parameter sent to the client:
+ 'delete_type' => 'DELETE',
+ 'access_control_allow_origin' => '*',
+ 'access_control_allow_credentials' => false,
+ 'access_control_allow_methods' => array(
+ 'OPTIONS',
+ 'HEAD',
+ 'GET',
+ 'POST',
+ 'PUT',
+ 'PATCH',
+ 'DELETE'
+ ),
+ 'access_control_allow_headers' => array(
+ 'Content-Type',
+ 'Content-Range',
+ 'Content-Disposition'
+ ),
+ // By default, allow redirects to the referer protocol+host:
+ 'redirect_allow_target' => '/^'.preg_quote(
+ parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_SCHEME)
+ .'://'
+ .parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_HOST)
+ .'/', // Trailing slash to not match subdomains by mistake
+ '/' // preg_quote delimiter param
+ ).'/',
+ // Enable to provide file downloads via GET requests to the PHP script:
+ // 1. Set to 1 to download files via readfile method through PHP
+ // 2. Set to 2 to send a X-Sendfile header for lighttpd/Apache
+ // 3. Set to 3 to send a X-Accel-Redirect header for nginx
+ // If set to 2 or 3, adjust the upload_url option to the base path of
+ // the redirect parameter, e.g. '/files/'.
+ 'download_via_php' => false,
+ // Read files in chunks to avoid memory limits when download_via_php
+ // is enabled, set to 0 to disable chunked reading of files:
+ 'readfile_chunk_size' => 10 * 1024 * 1024, // 10 MiB
+ // Defines which files can be displayed inline when downloaded:
+ 'inline_file_types' => '/\.(gif|jpe?g|png)$/i',
+ // Defines which files (based on their names) are accepted for upload.
+ // By default, only allows file uploads with image file extensions.
+ // Only change this setting after making sure that any allowed file
+ // types cannot be executed by the webserver in the files directory,
+ // e.g. PHP scripts, nor executed by the browser when downloaded,
+ // e.g. HTML files with embedded JavaScript code.
+ // Please also read the SECURITY.md document in this repository.
+ 'accept_file_types' => '/\.(gif|jpe?g|png)$/i',
+ // Replaces dots in filenames with the given string.
+ // Can be disabled by setting it to false or an empty string.
+ // Note that this is a security feature for servers that support
+ // multiple file extensions, e.g. the Apache AddHandler Directive:
+ // https://httpd.apache.org/docs/current/mod/mod_mime.html#addhandler
+ // Before disabling it, make sure that files uploaded with multiple
+ // extensions cannot be executed by the webserver, e.g.
+ // "example.php.png" with embedded PHP code, nor executed by the
+ // browser when downloaded, e.g. "example.html.gif" with embedded
+ // JavaScript code.
+ 'replace_dots_in_filenames' => '-',
+ // The php.ini settings upload_max_filesize and post_max_size
+ // take precedence over the following max_file_size setting:
+ 'max_file_size' => null,
+ 'min_file_size' => 1,
+ // The maximum number of files for the upload directory:
+ 'max_number_of_files' => null,
+ // Reads first file bytes to identify and correct file extensions:
+ 'correct_image_extensions' => false,
+ // Image resolution restrictions:
+ 'max_width' => null,
+ 'max_height' => null,
+ 'min_width' => 1,
+ 'min_height' => 1,
+ // Set the following option to false to enable resumable uploads:
+ 'discard_aborted_uploads' => true,
+ // Set to 0 to use the GD library to scale and orient images,
+ // set to 1 to use imagick (if installed, falls back to GD),
+ // set to 2 to use the ImageMagick convert binary directly:
+ 'image_library' => 1,
+ // Uncomment the following to define an array of resource limits
+ // for imagick:
+ /*
+ 'imagick_resource_limits' => array(
+ imagick::RESOURCETYPE_MAP => 32,
+ imagick::RESOURCETYPE_MEMORY => 32
+ ),
+ */
+ // Command or path for to the ImageMagick convert binary:
+ 'convert_bin' => 'convert',
+ // Uncomment the following to add parameters in front of each
+ // ImageMagick convert call (the limit constraints seem only
+ // to have an effect if put in front):
+ /*
+ 'convert_params' => '-limit memory 32MiB -limit map 32MiB',
+ */
+ // Command or path for to the ImageMagick identify binary:
+ 'identify_bin' => 'identify',
+ 'image_versions' => array(
+ // The empty image version key defines options for the original image.
+ // Keep in mind: these image manipulations are inherited by all other image versions from this point onwards.
+ // Also note that the property 'no_cache' is not inherited, since it's not a manipulation.
+ '' => array(
+ // Automatically rotate images based on EXIF meta data:
+ 'auto_orient' => true
+ ),
+ // You can add arrays to generate different versions.
+ // The name of the key is the name of the version (example: 'medium').
+ // the array contains the options to apply.
+ /*
+ 'medium' => array(
+ 'max_width' => 800,
+ 'max_height' => 600
+ ),
+ */
+ 'thumbnail' => array(
+ // Uncomment the following to use a defined directory for the thumbnails
+ // instead of a subdirectory based on the version identifier.
+ // Make sure that this directory doesn't allow execution of files if you
+ // don't pose any restrictions on the type of uploaded files, e.g. by
+ // copying the .htaccess file from the files directory for Apache:
+ //'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/thumb/',
+ //'upload_url' => $this->get_full_url().'/thumb/',
+ // Uncomment the following to force the max
+ // dimensions and e.g. create square thumbnails:
+ // 'auto_orient' => true,
+ // 'crop' => true,
+ // 'jpeg_quality' => 70,
+ // 'no_cache' => true, (there's a caching option, but this remembers thumbnail sizes from a previous action!)
+ // 'strip' => true, (this strips EXIF tags, such as geolocation)
+ 'max_width' => 80, // either specify width, or set to 0. Then width is automatically adjusted - keeping aspect ratio to a specified max_height.
+ 'max_height' => 80 // either specify height, or set to 0. Then height is automatically adjusted - keeping aspect ratio to a specified max_width.
+ )
+ ),
+ 'print_response' => true
+ );
+ if ($options) {
+ $this->options = $options + $this->options;
+ }
+ if ($error_messages) {
+ $this->error_messages = $error_messages + $this->error_messages;
+ }
+ if ($initialize) {
+ $this->initialize();
+ }
+ }
+
+ protected function initialize() {
+ switch ($this->get_server_var('REQUEST_METHOD')) {
+ case 'OPTIONS':
+ case 'HEAD':
+ $this->head();
+ break;
+ case 'GET':
+ $this->get($this->options['print_response']);
+ break;
+ case 'PATCH':
+ case 'PUT':
+ case 'POST':
+ $this->post($this->options['print_response']);
+ break;
+ case 'DELETE':
+ $this->delete($this->options['print_response']);
+ break;
+ default:
+ $this->header('HTTP/1.1 405 Method Not Allowed');
+ }
+ }
+
+ protected function get_full_url() {
+ $https = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'on') === 0 ||
+ !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
+ strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0;
+ return
+ ($https ? 'https://' : 'http://').
+ (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : '').
+ (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME'].
+ ($https && $_SERVER['SERVER_PORT'] === 443 ||
+ $_SERVER['SERVER_PORT'] === 80 ? '' : ':'.$_SERVER['SERVER_PORT']))).
+ substr($_SERVER['SCRIPT_NAME'],0, strrpos($_SERVER['SCRIPT_NAME'], '/'));
+ }
+
+ protected function get_user_id() {
+ @session_start();
+ return session_id();
+ }
+
+ protected function get_user_path() {
+ if ($this->options['user_dirs']) {
+ return $this->get_user_id().'/';
+ }
+ return '';
+ }
+
+ protected function get_upload_path($file_name = null, $version = null) {
+ $file_name = $file_name ? $file_name : '';
+ if (empty($version)) {
+ $version_path = '';
+ } else {
+ $version_dir = @$this->options['image_versions'][$version]['upload_dir'];
+ if ($version_dir) {
+ return $version_dir.$this->get_user_path().$file_name;
+ }
+ $version_path = $version.'/';
+ }
+ return $this->options['upload_dir'].$this->get_user_path()
+ .$version_path.$file_name;
+ }
+
+ protected function get_query_separator($url) {
+ return strpos($url, '?') === false ? '?' : '&';
+ }
+
+ protected function get_download_url($file_name, $version = null, $direct = false) {
+ if (!$direct && $this->options['download_via_php']) {
+ $url = $this->options['script_url']
+ .$this->get_query_separator($this->options['script_url'])
+ .$this->get_singular_param_name()
+ .'='.rawurlencode($file_name);
+ if ($version) {
+ $url .= '&version='.rawurlencode($version);
+ }
+ return $url.'&download=1';
+ }
+ if (empty($version)) {
+ $version_path = '';
+ } else {
+ $version_url = @$this->options['image_versions'][$version]['upload_url'];
+ if ($version_url) {
+ return $version_url.$this->get_user_path().rawurlencode($file_name);
+ }
+ $version_path = rawurlencode($version).'/';
+ }
+ return $this->options['upload_url'].$this->get_user_path()
+ .$version_path.rawurlencode($file_name);
+ }
+
+ protected function set_additional_file_properties($file) {
+ $file->deleteUrl = $this->options['script_url']
+ .$this->get_query_separator($this->options['script_url'])
+ .$this->get_singular_param_name()
+ .'='.rawurlencode($file->name);
+ $file->deleteType = $this->options['delete_type'];
+ if ($file->deleteType !== 'DELETE') {
+ $file->deleteUrl .= '&_method=DELETE';
+ }
+ if ($this->options['access_control_allow_credentials']) {
+ $file->deleteWithCredentials = true;
+ }
+ }
+
+ // Fix for overflowing signed 32 bit integers,
+ // works for sizes up to 2^32-1 bytes (4 GiB - 1):
+ protected function fix_integer_overflow($size) {
+ if ($size < 0) {
+ $size += 2.0 * (PHP_INT_MAX + 1);
+ }
+ return $size;
+ }
+
+ protected function get_file_size($file_path, $clear_stat_cache = false) {
+ if ($clear_stat_cache) {
+ if (version_compare(PHP_VERSION, '5.3.0') >= 0) {
+ clearstatcache(true, $file_path);
+ } else {
+ clearstatcache();
+ }
+ }
+ return $this->fix_integer_overflow(filesize($file_path));
+ }
+
+ protected function is_valid_file_object($file_name) {
+ $file_path = $this->get_upload_path($file_name);
+ if (strlen($file_name) > 0 && $file_name[0] !== '.' && is_file($file_path)) {
+ return true;
+ }
+ return false;
+ }
+
+ protected function get_file_object($file_name) {
+ if ($this->is_valid_file_object($file_name)) {
+ $file = new \stdClass();
+ $file->name = $file_name;
+ $file->size = $this->get_file_size(
+ $this->get_upload_path($file_name)
+ );
+ $file->url = $this->get_download_url($file->name);
+ foreach ($this->options['image_versions'] as $version => $options) {
+ if (!empty($version)) {
+ if (is_file($this->get_upload_path($file_name, $version))) {
+ $file->{$version.'Url'} = $this->get_download_url(
+ $file->name,
+ $version
+ );
+ }
+ }
+ }
+ $this->set_additional_file_properties($file);
+ return $file;
+ }
+ return null;
+ }
+
+ protected function get_file_objects($iteration_method = 'get_file_object') {
+ $upload_dir = $this->get_upload_path();
+ if (!is_dir($upload_dir)) {
+ return array();
+ }
+ return array_values(array_filter(array_map(
+ array($this, $iteration_method),
+ scandir($upload_dir)
+ )));
+ }
+
+ protected function count_file_objects() {
+ return count($this->get_file_objects('is_valid_file_object'));
+ }
+
+ protected function get_error_message($error) {
+ return isset($this->error_messages[$error]) ?
+ $this->error_messages[$error] : $error;
+ }
+
+ public function get_config_bytes($val) {
+ $val = trim($val);
+ $last = strtolower($val[strlen($val)-1]);
+ if (is_numeric($val)) {
+ $val = (int)$val;
+ } else {
+ $val = (int)substr($val, 0, -1);
+ }
+ switch ($last) {
+ case 'g':
+ $val *= 1024;
+ case 'm':
+ $val *= 1024;
+ case 'k':
+ $val *= 1024;
+ }
+ return $this->fix_integer_overflow($val);
+ }
+
+ protected function validate_image_file($uploaded_file, $file, $error, $index) {
+ if ($this->imagetype($uploaded_file) !== $this->get_file_type($file->name)) {
+ $file->error = $this->get_error_message('invalid_file_type');
+ return false;
+ }
+ $max_width = @$this->options['max_width'];
+ $max_height = @$this->options['max_height'];
+ $min_width = @$this->options['min_width'];
+ $min_height = @$this->options['min_height'];
+ if ($max_width || $max_height || $min_width || $min_height) {
+ list($img_width, $img_height) = $this->get_image_size($uploaded_file);
+ // If we are auto rotating the image by default, do the checks on
+ // the correct orientation
+ if (
+ @$this->options['image_versions']['']['auto_orient'] &&
+ function_exists('exif_read_data') &&
+ ($exif = @exif_read_data($uploaded_file)) &&
+ (((int) @$exif['Orientation']) >= 5)
+ ) {
+ $tmp = $img_width;
+ $img_width = $img_height;
+ $img_height = $tmp;
+ unset($tmp);
+ }
+ if (!empty($img_width) && !empty($img_height)) {
+ if ($max_width && $img_width > $max_width) {
+ $file->error = $this->get_error_message('max_width');
+ return false;
+ }
+ if ($max_height && $img_height > $max_height) {
+ $file->error = $this->get_error_message('max_height');
+ return false;
+ }
+ if ($min_width && $img_width < $min_width) {
+ $file->error = $this->get_error_message('min_width');
+ return false;
+ }
+ if ($min_height && $img_height < $min_height) {
+ $file->error = $this->get_error_message('min_height');
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ protected function validate($uploaded_file, $file, $error, $index, $content_range) {
+ if ($error) {
+ $file->error = $this->get_error_message($error);
+ return false;
+ }
+ $content_length = $this->fix_integer_overflow(
+ (int)$this->get_server_var('CONTENT_LENGTH')
+ );
+ $post_max_size = $this->get_config_bytes(ini_get('post_max_size'));
+ if ($post_max_size && ($content_length > $post_max_size)) {
+ $file->error = $this->get_error_message('post_max_size');
+ return false;
+ }
+ if (!preg_match($this->options['accept_file_types'], $file->name)) {
+ $file->error = $this->get_error_message('accept_file_types');
+ return false;
+ }
+ if ($uploaded_file && is_uploaded_file($uploaded_file)) {
+ $file_size = $this->get_file_size($uploaded_file);
+ } else {
+ $file_size = $content_length;
+ }
+ if ($this->options['max_file_size'] && (
+ $file_size > $this->options['max_file_size'] ||
+ $file->size > $this->options['max_file_size'])
+ ) {
+ $file->error = $this->get_error_message('max_file_size');
+ return false;
+ }
+ if ($this->options['min_file_size'] &&
+ $file_size < $this->options['min_file_size']) {
+ $file->error = $this->get_error_message('min_file_size');
+ return false;
+ }
+ if (is_int($this->options['max_number_of_files']) &&
+ ($this->count_file_objects() >= $this->options['max_number_of_files']) &&
+ // Ignore additional chunks of existing files:
+ !is_file($this->get_upload_path($file->name))) {
+ $file->error = $this->get_error_message('max_number_of_files');
+ return false;
+ }
+ if (!$content_range && $this->has_image_file_extension($file->name)) {
+ return $this->validate_image_file($uploaded_file, $file, $error, $index);
+ }
+ return true;
+ }
+
+ protected function upcount_name_callback($matches) {
+ $index = isset($matches[1]) ? ((int)$matches[1]) + 1 : 1;
+ $ext = isset($matches[2]) ? $matches[2] : '';
+ return ' ('.$index.')'.$ext;
+ }
+
+ protected function upcount_name($name) {
+ return preg_replace_callback(
+ '/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/',
+ array($this, 'upcount_name_callback'),
+ $name,
+ 1
+ );
+ }
+
+ protected function get_unique_filename($file_path, $name, $size, $type, $error,
+ $index, $content_range) {
+ while(is_dir($this->get_upload_path($name))) {
+ $name = $this->upcount_name($name);
+ }
+ // Keep an existing filename if this is part of a chunked upload:
+ $uploaded_bytes = $this->fix_integer_overflow((int)@$content_range[1]);
+ while (is_file($this->get_upload_path($name))) {
+ if ($uploaded_bytes === $this->get_file_size(
+ $this->get_upload_path($name))) {
+ break;
+ }
+ $name = $this->upcount_name($name);
+ }
+ return $name;
+ }
+
+ protected function get_valid_image_extensions($file_path) {
+ switch ($this->imagetype($file_path)) {
+ case self::IMAGETYPE_JPEG:
+ return array('jpg', 'jpeg');
+ case self::IMAGETYPE_PNG:
+ return array('png');
+ case self::IMAGETYPE_GIF:
+ return array('gif');
+ }
+ }
+
+ protected function fix_file_extension($file_path, $name, $size, $type, $error,
+ $index, $content_range) {
+ // Add missing file extension for known image types:
+ if (strpos($name, '.') === false &&
+ preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
+ $name .= '.'.$matches[1];
+ }
+ if ($this->options['correct_image_extensions']) {
+ $extensions = $this->get_valid_image_extensions($file_path);
+ // Adjust incorrect image file extensions:
+ if (!empty($extensions)) {
+ $parts = explode('.', $name);
+ $extIndex = count($parts) - 1;
+ $ext = strtolower(@$parts[$extIndex]);
+ if (!in_array($ext, $extensions)) {
+ $parts[$extIndex] = $extensions[0];
+ $name = implode('.', $parts);
+ }
+ }
+ }
+ return $name;
+ }
+
+ protected function trim_file_name($file_path, $name, $size, $type, $error,
+ $index, $content_range) {
+ // Remove path information and dots around the filename, to prevent uploading
+ // into different directories or replacing hidden system files.
+ // Also remove control characters and spaces (\x00..\x20) around the filename:
+ $name = trim($this->basename(stripslashes($name)), ".\x00..\x20");
+ // Replace dots in filenames to avoid security issues with servers
+ // that interpret multiple file extensions, e.g. "example.php.png":
+ $replacement = $this->options['replace_dots_in_filenames'];
+ if (!empty($replacement)) {
+ $parts = explode('.', $name);
+ if (count($parts) > 2) {
+ $ext = array_pop($parts);
+ $name = implode($replacement, $parts).'.'.$ext;
+ }
+ }
+ // Use a timestamp for empty filenames:
+ if (!$name) {
+ $name = str_replace('.', '-', microtime(true));
+ }
+ return $name;
+ }
+
+ protected function get_file_name($file_path, $name, $size, $type, $error,
+ $index, $content_range) {
+ $name = $this->trim_file_name($file_path, $name, $size, $type, $error,
+ $index, $content_range);
+ return $this->get_unique_filename(
+ $file_path,
+ $this->fix_file_extension($file_path, $name, $size, $type, $error,
+ $index, $content_range),
+ $size,
+ $type,
+ $error,
+ $index,
+ $content_range
+ );
+ }
+
+ protected function get_scaled_image_file_paths($file_name, $version) {
+ $file_path = $this->get_upload_path($file_name);
+ if (!empty($version)) {
+ $version_dir = $this->get_upload_path(null, $version);
+ if (!is_dir($version_dir)) {
+ mkdir($version_dir, $this->options['mkdir_mode'], true);
+ }
+ $new_file_path = $version_dir.'/'.$file_name;
+ } else {
+ $new_file_path = $file_path;
+ }
+ return array($file_path, $new_file_path);
+ }
+
+ protected function gd_get_image_object($file_path, $func, $no_cache = false) {
+ if (empty($this->image_objects[$file_path]) || $no_cache) {
+ $this->gd_destroy_image_object($file_path);
+ $this->image_objects[$file_path] = $func($file_path);
+ }
+ return $this->image_objects[$file_path];
+ }
+
+ protected function gd_set_image_object($file_path, $image) {
+ $this->gd_destroy_image_object($file_path);
+ $this->image_objects[$file_path] = $image;
+ }
+
+ protected function gd_destroy_image_object($file_path) {
+ $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ;
+ return $image && imagedestroy($image);
+ }
+
+ protected function gd_imageflip($image, $mode) {
+ if (function_exists('imageflip')) {
+ return imageflip($image, $mode);
+ }
+ $new_width = $src_width = imagesx($image);
+ $new_height = $src_height = imagesy($image);
+ $new_img = imagecreatetruecolor($new_width, $new_height);
+ $src_x = 0;
+ $src_y = 0;
+ switch ($mode) {
+ case '1': // flip on the horizontal axis
+ $src_y = $new_height - 1;
+ $src_height = -$new_height;
+ break;
+ case '2': // flip on the vertical axis
+ $src_x = $new_width - 1;
+ $src_width = -$new_width;
+ break;
+ case '3': // flip on both axes
+ $src_y = $new_height - 1;
+ $src_height = -$new_height;
+ $src_x = $new_width - 1;
+ $src_width = -$new_width;
+ break;
+ default:
+ return $image;
+ }
+ imagecopyresampled(
+ $new_img,
+ $image,
+ 0,
+ 0,
+ $src_x,
+ $src_y,
+ $new_width,
+ $new_height,
+ $src_width,
+ $src_height
+ );
+ return $new_img;
+ }
+
+ protected function gd_orient_image($file_path, $src_img) {
+ if (!function_exists('exif_read_data')) {
+ return false;
+ }
+ $exif = @exif_read_data($file_path);
+ if ($exif === false) {
+ return false;
+ }
+ $orientation = (int)@$exif['Orientation'];
+ if ($orientation < 2 || $orientation > 8) {
+ return false;
+ }
+ switch ($orientation) {
+ case 2:
+ $new_img = $this->gd_imageflip(
+ $src_img,
+ defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2
+ );
+ break;
+ case 3:
+ $new_img = imagerotate($src_img, 180, 0);
+ break;
+ case 4:
+ $new_img = $this->gd_imageflip(
+ $src_img,
+ defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1
+ );
+ break;
+ case 5:
+ $tmp_img = $this->gd_imageflip(
+ $src_img,
+ defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1
+ );
+ $new_img = imagerotate($tmp_img, 270, 0);
+ imagedestroy($tmp_img);
+ break;
+ case 6:
+ $new_img = imagerotate($src_img, 270, 0);
+ break;
+ case 7:
+ $tmp_img = $this->gd_imageflip(
+ $src_img,
+ defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2
+ );
+ $new_img = imagerotate($tmp_img, 270, 0);
+ imagedestroy($tmp_img);
+ break;
+ case 8:
+ $new_img = imagerotate($src_img, 90, 0);
+ break;
+ default:
+ return false;
+ }
+ $this->gd_set_image_object($file_path, $new_img);
+ return true;
+ }
+
+ protected function gd_create_scaled_image($file_name, $version, $options) {
+ if (!function_exists('imagecreatetruecolor')) {
+ error_log('Function not found: imagecreatetruecolor');
+ return false;
+ }
+ list($file_path, $new_file_path) =
+ $this->get_scaled_image_file_paths($file_name, $version);
+ $type = strtolower(substr(strrchr($file_name, '.'), 1));
+ switch ($type) {
+ case 'jpg':
+ case 'jpeg':
+ $src_func = 'imagecreatefromjpeg';
+ $write_func = 'imagejpeg';
+ $image_quality = isset($options['jpeg_quality']) ?
+ $options['jpeg_quality'] : 75;
+ break;
+ case 'gif':
+ $src_func = 'imagecreatefromgif';
+ $write_func = 'imagegif';
+ $image_quality = null;
+ break;
+ case 'png':
+ $src_func = 'imagecreatefrompng';
+ $write_func = 'imagepng';
+ $image_quality = isset($options['png_quality']) ?
+ $options['png_quality'] : 9;
+ break;
+ default:
+ return false;
+ }
+ $src_img = $this->gd_get_image_object(
+ $file_path,
+ $src_func,
+ !empty($options['no_cache'])
+ );
+ $image_oriented = false;
+ if (!empty($options['auto_orient']) && $this->gd_orient_image(
+ $file_path,
+ $src_img
+ )) {
+ $image_oriented = true;
+ $src_img = $this->gd_get_image_object(
+ $file_path,
+ $src_func
+ );
+ }
+ $max_width = $img_width = imagesx($src_img);
+ $max_height = $img_height = imagesy($src_img);
+ if (!empty($options['max_width'])) {
+ $max_width = $options['max_width'];
+ }
+ if (!empty($options['max_height'])) {
+ $max_height = $options['max_height'];
+ }
+ $scale = min(
+ $max_width / $img_width,
+ $max_height / $img_height
+ );
+ if ($scale >= 1) {
+ if ($image_oriented) {
+ return $write_func($src_img, $new_file_path, $image_quality);
+ }
+ if ($file_path !== $new_file_path) {
+ return copy($file_path, $new_file_path);
+ }
+ return true;
+ }
+ if (empty($options['crop'])) {
+ $new_width = $img_width * $scale;
+ $new_height = $img_height * $scale;
+ $dst_x = 0;
+ $dst_y = 0;
+ $new_img = imagecreatetruecolor($new_width, $new_height);
+ } else {
+ if (($img_width / $img_height) >= ($max_width / $max_height)) {
+ $new_width = $img_width / ($img_height / $max_height);
+ $new_height = $max_height;
+ } else {
+ $new_width = $max_width;
+ $new_height = $img_height / ($img_width / $max_width);
+ }
+ $dst_x = 0 - ($new_width - $max_width) / 2;
+ $dst_y = 0 - ($new_height - $max_height) / 2;
+ $new_img = imagecreatetruecolor($max_width, $max_height);
+ }
+ // Handle transparency in GIF and PNG images:
+ switch ($type) {
+ case 'gif':
+ imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0));
+ break;
+ case 'png':
+ imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0));
+ imagealphablending($new_img, false);
+ imagesavealpha($new_img, true);
+ break;
+ }
+ $success = imagecopyresampled(
+ $new_img,
+ $src_img,
+ $dst_x,
+ $dst_y,
+ 0,
+ 0,
+ $new_width,
+ $new_height,
+ $img_width,
+ $img_height
+ ) && $write_func($new_img, $new_file_path, $image_quality);
+ $this->gd_set_image_object($file_path, $new_img);
+ return $success;
+ }
+
+ protected function imagick_get_image_object($file_path, $no_cache = false) {
+ if (empty($this->image_objects[$file_path]) || $no_cache) {
+ $this->imagick_destroy_image_object($file_path);
+ $image = new \Imagick();
+ if (!empty($this->options['imagick_resource_limits'])) {
+ foreach ($this->options['imagick_resource_limits'] as $type => $limit) {
+ $image->setResourceLimit($type, $limit);
+ }
+ }
+ try {
+ $image->readImage($file_path);
+ } catch (\ImagickException $e) {
+ error_log($e->getMessage());
+ return null;
+ }
+ $this->image_objects[$file_path] = $image;
+ }
+ return $this->image_objects[$file_path];
+ }
+
+ protected function imagick_set_image_object($file_path, $image) {
+ $this->imagick_destroy_image_object($file_path);
+ $this->image_objects[$file_path] = $image;
+ }
+
+ protected function imagick_destroy_image_object($file_path) {
+ $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ;
+ return $image && $image->destroy();
+ }
+
+ protected function imagick_orient_image($image) {
+ $orientation = $image->getImageOrientation();
+ $background = new \ImagickPixel('none');
+ switch ($orientation) {
+ case \imagick::ORIENTATION_TOPRIGHT: // 2
+ $image->flopImage(); // horizontal flop around y-axis
+ break;
+ case \imagick::ORIENTATION_BOTTOMRIGHT: // 3
+ $image->rotateImage($background, 180);
+ break;
+ case \imagick::ORIENTATION_BOTTOMLEFT: // 4
+ $image->flipImage(); // vertical flip around x-axis
+ break;
+ case \imagick::ORIENTATION_LEFTTOP: // 5
+ $image->flopImage(); // horizontal flop around y-axis
+ $image->rotateImage($background, 270);
+ break;
+ case \imagick::ORIENTATION_RIGHTTOP: // 6
+ $image->rotateImage($background, 90);
+ break;
+ case \imagick::ORIENTATION_RIGHTBOTTOM: // 7
+ $image->flipImage(); // vertical flip around x-axis
+ $image->rotateImage($background, 270);
+ break;
+ case \imagick::ORIENTATION_LEFTBOTTOM: // 8
+ $image->rotateImage($background, 270);
+ break;
+ default:
+ return false;
+ }
+ $image->setImageOrientation(\imagick::ORIENTATION_TOPLEFT); // 1
+ return true;
+ }
+
+ protected function imagick_create_scaled_image($file_name, $version, $options) {
+ list($file_path, $new_file_path) =
+ $this->get_scaled_image_file_paths($file_name, $version);
+ $image = $this->imagick_get_image_object(
+ $file_path,
+ !empty($options['crop']) || !empty($options['no_cache'])
+ );
+ if (is_null($image)) return false;
+ if ($image->getImageFormat() === 'GIF') {
+ // Handle animated GIFs:
+ $images = $image->coalesceImages();
+ foreach ($images as $frame) {
+ $image = $frame;
+ $this->imagick_set_image_object($file_name, $image);
+ break;
+ }
+ }
+ $image_oriented = false;
+ if (!empty($options['auto_orient'])) {
+ $image_oriented = $this->imagick_orient_image($image);
+ }
+ $image_resize = false;
+ $new_width = $max_width = $img_width = $image->getImageWidth();
+ $new_height = $max_height = $img_height = $image->getImageHeight();
+ // use isset(). User might be setting max_width = 0 (auto in regular resizing). Value 0 would be considered empty when you use empty()
+ if (isset($options['max_width'])) {
+ $image_resize = true;
+ $new_width = $max_width = $options['max_width'];
+ }
+ if (isset($options['max_height'])) {
+ $image_resize = true;
+ $new_height = $max_height = $options['max_height'];
+ }
+ $image_strip = (isset($options['strip']) ? $options['strip'] : false);
+ if ( !$image_oriented && ($max_width >= $img_width) && ($max_height >= $img_height) && !$image_strip && empty($options["jpeg_quality"]) ) {
+ if ($file_path !== $new_file_path) {
+ return copy($file_path, $new_file_path);
+ }
+ return true;
+ }
+ $crop = (isset($options['crop']) ? $options['crop'] : false);
+
+ if ($crop) {
+ $x = 0;
+ $y = 0;
+ if (($img_width / $img_height) >= ($max_width / $max_height)) {
+ $new_width = 0; // Enables proportional scaling based on max_height
+ $x = ($img_width / ($img_height / $max_height) - $max_width) / 2;
+ } else {
+ $new_height = 0; // Enables proportional scaling based on max_width
+ $y = ($img_height / ($img_width / $max_width) - $max_height) / 2;
+ }
+ }
+ $success = $image->resizeImage(
+ $new_width,
+ $new_height,
+ isset($options['filter']) ? $options['filter'] : \imagick::FILTER_LANCZOS,
+ isset($options['blur']) ? $options['blur'] : 1,
+ $new_width && $new_height // fit image into constraints if not to be cropped
+ );
+ if ($success && $crop) {
+ $success = $image->cropImage(
+ $max_width,
+ $max_height,
+ $x,
+ $y
+ );
+ if ($success) {
+ $success = $image->setImagePage($max_width, $max_height, 0, 0);
+ }
+ }
+ $type = strtolower(substr(strrchr($file_name, '.'), 1));
+ switch ($type) {
+ case 'jpg':
+ case 'jpeg':
+ if (!empty($options['jpeg_quality'])) {
+ $image->setImageCompression(\imagick::COMPRESSION_JPEG);
+ $image->setImageCompressionQuality($options['jpeg_quality']);
+ }
+ break;
+ }
+ if ( $image_strip ) {
+ $image->stripImage();
+ }
+ return $success && $image->writeImage($new_file_path);
+ }
+
+ protected function imagemagick_create_scaled_image($file_name, $version, $options) {
+ list($file_path, $new_file_path) =
+ $this->get_scaled_image_file_paths($file_name, $version);
+ $resize = @$options['max_width']
+ .(empty($options['max_height']) ? '' : 'X'.$options['max_height']);
+ if (!$resize && empty($options['auto_orient'])) {
+ if ($file_path !== $new_file_path) {
+ return copy($file_path, $new_file_path);
+ }
+ return true;
+ }
+ $cmd = $this->options['convert_bin'];
+ if (!empty($this->options['convert_params'])) {
+ $cmd .= ' '.$this->options['convert_params'];
+ }
+ $cmd .= ' '.escapeshellarg($file_path);
+ if (!empty($options['auto_orient'])) {
+ $cmd .= ' -auto-orient';
+ }
+ if ($resize) {
+ // Handle animated GIFs:
+ $cmd .= ' -coalesce';
+ if (empty($options['crop'])) {
+ $cmd .= ' -resize '.escapeshellarg($resize.'>');
+ } else {
+ $cmd .= ' -resize '.escapeshellarg($resize.'^');
+ $cmd .= ' -gravity center';
+ $cmd .= ' -crop '.escapeshellarg($resize.'+0+0');
+ }
+ // Make sure the page dimensions are correct (fixes offsets of animated GIFs):
+ $cmd .= ' +repage';
+ }
+ if (!empty($options['convert_params'])) {
+ $cmd .= ' '.$options['convert_params'];
+ }
+ $cmd .= ' '.escapeshellarg($new_file_path);
+ exec($cmd, $output, $error);
+ if ($error) {
+ error_log(implode('\n', $output));
+ return false;
+ }
+ return true;
+ }
+
+ protected function get_image_size($file_path) {
+ if ($this->options['image_library']) {
+ if (extension_loaded('imagick')) {
+ $image = new \Imagick();
+ try {
+ if (@$image->pingImage($file_path)) {
+ $dimensions = array($image->getImageWidth(), $image->getImageHeight());
+ $image->destroy();
+ return $dimensions;
+ }
+ return false;
+ } catch (\Exception $e) {
+ error_log($e->getMessage());
+ }
+ }
+ if ($this->options['image_library'] === 2) {
+ $cmd = $this->options['identify_bin'];
+ $cmd .= ' -ping '.escapeshellarg($file_path);
+ exec($cmd, $output, $error);
+ if (!$error && !empty($output)) {
+ // image.jpg JPEG 1920x1080 1920x1080+0+0 8-bit sRGB 465KB 0.000u 0:00.000
+ $infos = preg_split('/\s+/', substr($output[0], strlen($file_path)));
+ $dimensions = preg_split('/x/', $infos[2]);
+ return $dimensions;
+ }
+ return false;
+ }
+ }
+ if (!function_exists('getimagesize')) {
+ error_log('Function not found: getimagesize');
+ return false;
+ }
+ return @getimagesize($file_path);
+ }
+
+ protected function create_scaled_image($file_name, $version, $options) {
+ try {
+ if ($this->options['image_library'] === 2) {
+ return $this->imagemagick_create_scaled_image($file_name, $version, $options);
+ }
+ if ($this->options['image_library'] && extension_loaded('imagick')) {
+ return $this->imagick_create_scaled_image($file_name, $version, $options);
+ }
+ return $this->gd_create_scaled_image($file_name, $version, $options);
+ } catch (\Exception $e) {
+ error_log($e->getMessage());
+ return false;
+ }
+ }
+
+ protected function destroy_image_object($file_path) {
+ if ($this->options['image_library'] && extension_loaded('imagick')) {
+ return $this->imagick_destroy_image_object($file_path);
+ }
+ }
+
+ protected function imagetype($file_path) {
+ $fp = fopen($file_path, 'r');
+ $data = fread($fp, 4);
+ fclose($fp);
+ // GIF: 47 49 46 38
+ if ($data === 'GIF8') {
+ return self::IMAGETYPE_GIF;
+ }
+ // JPG: FF D8 FF
+ if (bin2hex(substr($data, 0, 3)) === 'ffd8ff') {
+ return self::IMAGETYPE_JPEG;
+ }
+ // PNG: 89 50 4E 47
+ if (bin2hex(@$data[0]).substr($data, 1, 4) === '89PNG') {
+ return self::IMAGETYPE_PNG;
+ }
+ return false;
+ }
+
+ protected function is_valid_image_file($file_path) {
+ return !!$this->imagetype($file_path);
+ }
+
+ protected function has_image_file_extension($file_path) {
+ return !!preg_match('/\.(gif|jpe?g|png)$/i', $file_path);
+ }
+
+ protected function handle_image_file($file_path, $file) {
+ $failed_versions = array();
+ foreach ($this->options['image_versions'] as $version => $options) {
+ if ($this->create_scaled_image($file->name, $version, $options)) {
+ if (!empty($version)) {
+ $file->{$version.'Url'} = $this->get_download_url(
+ $file->name,
+ $version
+ );
+ } else {
+ $file->size = $this->get_file_size($file_path, true);
+ }
+ } else {
+ $failed_versions[] = $version ? $version : 'original';
+ }
+ }
+ if (count($failed_versions)) {
+ $file->error = $this->get_error_message('image_resize')
+ .' ('.implode(', ', $failed_versions).')';
+ }
+ // Free memory:
+ $this->destroy_image_object($file_path);
+ }
+
+ protected function handle_file_upload($uploaded_file, $name, $size, $type, $error,
+ $index = null, $content_range = null) {
+ $file = new \stdClass();
+ $file->name = $this->get_file_name($uploaded_file, $name, $size, $type, $error,
+ $index, $content_range);
+ $file->size = $this->fix_integer_overflow((int)$size);
+ $file->type = $type;
+ if ($this->validate($uploaded_file, $file, $error, $index, $content_range)) {
+ $this->handle_form_data($file, $index);
+ $upload_dir = $this->get_upload_path();
+ if (!is_dir($upload_dir)) {
+ mkdir($upload_dir, $this->options['mkdir_mode'], true);
+ }
+ $file_path = $this->get_upload_path($file->name);
+ $append_file = $content_range && is_file($file_path) &&
+ $file->size > $this->get_file_size($file_path);
+ if ($uploaded_file && is_uploaded_file($uploaded_file)) {
+ // multipart/formdata uploads (POST method uploads)
+ if ($append_file) {
+ file_put_contents(
+ $file_path,
+ fopen($uploaded_file, 'r'),
+ FILE_APPEND
+ );
+ } else {
+ move_uploaded_file($uploaded_file, $file_path);
+ }
+ } else {
+ // Non-multipart uploads (PUT method support)
+ file_put_contents(
+ $file_path,
+ fopen($this->options['input_stream'], 'r'),
+ $append_file ? FILE_APPEND : 0
+ );
+ }
+ $file_size = $this->get_file_size($file_path, $append_file);
+ if ($file_size === $file->size) {
+ $file->url = $this->get_download_url($file->name);
+ if ($this->has_image_file_extension($file->name)) {
+ if ($content_range && !$this->validate_image_file($file_path, $file, $error, $index)) {
+ unlink($file_path);
+ } else {
+ $this->handle_image_file($file_path, $file);
+ }
+ }
+ } else {
+ $file->size = $file_size;
+ if (!$content_range && $this->options['discard_aborted_uploads']) {
+ unlink($file_path);
+ $file->error = $this->get_error_message('abort');
+ }
+ }
+ $this->set_additional_file_properties($file);
+ }
+ return $file;
+ }
+
+ protected function readfile($file_path) {
+ $file_size = $this->get_file_size($file_path);
+ $chunk_size = $this->options['readfile_chunk_size'];
+ if ($chunk_size && $file_size > $chunk_size) {
+ $handle = fopen($file_path, 'rb');
+ while (!feof($handle)) {
+ echo fread($handle, $chunk_size);
+ @ob_flush();
+ @flush();
+ }
+ fclose($handle);
+ return $file_size;
+ }
+ return readfile($file_path);
+ }
+
+ protected function body($str) {
+ echo $str;
+ }
+
+ protected function header($str) {
+ header($str);
+ }
+
+ protected function get_upload_data($id) {
+ return @$_FILES[$id];
+ }
+
+ protected function get_post_param($id) {
+ return @$_POST[$id];
+ }
+
+ protected function get_query_param($id) {
+ return @$_GET[$id];
+ }
+
+ protected function get_server_var($id) {
+ return @$_SERVER[$id];
+ }
+
+ protected function handle_form_data($file, $index) {
+ // Handle form data, e.g. $_POST['description'][$index]
+ }
+
+ protected function get_version_param() {
+ return $this->basename(stripslashes($this->get_query_param('version')));
+ }
+
+ protected function get_singular_param_name() {
+ return substr($this->options['param_name'], 0, -1);
+ }
+
+ protected function get_file_name_param() {
+ $name = $this->get_singular_param_name();
+ return $this->basename(stripslashes($this->get_query_param($name)));
+ }
+
+ protected function get_file_names_params() {
+ $params = $this->get_query_param($this->options['param_name']);
+ if (!$params) {
+ return null;
+ }
+ foreach ($params as $key => $value) {
+ $params[$key] = $this->basename(stripslashes($value));
+ }
+ return $params;
+ }
+
+ protected function get_file_type($file_path) {
+ switch (strtolower(pathinfo($file_path, PATHINFO_EXTENSION))) {
+ case 'jpeg':
+ case 'jpg':
+ return self::IMAGETYPE_JPEG;
+ case 'png':
+ return self::IMAGETYPE_PNG;
+ case 'gif':
+ return self::IMAGETYPE_GIF;
+ default:
+ return '';
+ }
+ }
+
+ protected function download() {
+ switch ($this->options['download_via_php']) {
+ case 1:
+ $redirect_header = null;
+ break;
+ case 2:
+ $redirect_header = 'X-Sendfile';
+ break;
+ case 3:
+ $redirect_header = 'X-Accel-Redirect';
+ break;
+ default:
+ return $this->header('HTTP/1.1 403 Forbidden');
+ }
+ $file_name = $this->get_file_name_param();
+ if (!$this->is_valid_file_object($file_name)) {
+ return $this->header('HTTP/1.1 404 Not Found');
+ }
+ if ($redirect_header) {
+ return $this->header(
+ $redirect_header.': '.$this->get_download_url(
+ $file_name,
+ $this->get_version_param(),
+ true
+ )
+ );
+ }
+ $file_path = $this->get_upload_path($file_name, $this->get_version_param());
+ // Prevent browsers from MIME-sniffing the content-type:
+ $this->header('X-Content-Type-Options: nosniff');
+ if (!preg_match($this->options['inline_file_types'], $file_name)) {
+ $this->header('Content-Type: application/octet-stream');
+ $this->header('Content-Disposition: attachment; filename="'.$file_name.'"');
+ } else {
+ $this->header('Content-Type: '.$this->get_file_type($file_path));
+ $this->header('Content-Disposition: inline; filename="'.$file_name.'"');
+ }
+ $this->header('Content-Length: '.$this->get_file_size($file_path));
+ $this->header('Last-Modified: '.gmdate('D, d M Y H:i:s T', filemtime($file_path)));
+ $this->readfile($file_path);
+ }
+
+ protected function send_content_type_header() {
+ $this->header('Vary: Accept');
+ if (strpos($this->get_server_var('HTTP_ACCEPT'), 'application/json') !== false) {
+ $this->header('Content-type: application/json');
+ } else {
+ $this->header('Content-type: text/plain');
+ }
+ }
+
+ protected function send_access_control_headers() {
+ $this->header('Access-Control-Allow-Origin: '.$this->options['access_control_allow_origin']);
+ $this->header('Access-Control-Allow-Credentials: '
+ .($this->options['access_control_allow_credentials'] ? 'true' : 'false'));
+ $this->header('Access-Control-Allow-Methods: '
+ .implode(', ', $this->options['access_control_allow_methods']));
+ $this->header('Access-Control-Allow-Headers: '
+ .implode(', ', $this->options['access_control_allow_headers']));
+ }
+
+ public function generate_response($content, $print_response = true) {
+ $this->response = $content;
+ if ($print_response) {
+ $json = json_encode($content);
+ $redirect = stripslashes($this->get_post_param('redirect'));
+ if ($redirect && preg_match($this->options['redirect_allow_target'], $redirect)) {
+ return $this->header('Location: '.sprintf($redirect, rawurlencode($json)));
+ }
+ $this->head();
+ if ($this->get_server_var('HTTP_CONTENT_RANGE')) {
+ $files = isset($content[$this->options['param_name']]) ?
+ $content[$this->options['param_name']] : null;
+ if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) {
+ $this->header('Range: 0-'.(
+ $this->fix_integer_overflow((int)$files[0]->size) - 1
+ ));
+ }
+ }
+ $this->body($json);
+ }
+ return $content;
+ }
+
+ public function get_response () {
+ return $this->response;
+ }
+
+ public function head() {
+ $this->header('Pragma: no-cache');
+ $this->header('Cache-Control: no-store, no-cache, must-revalidate');
+ $this->header('Content-Disposition: inline; filename="files.json"');
+ // Prevent Internet Explorer from MIME-sniffing the content-type:
+ $this->header('X-Content-Type-Options: nosniff');
+ if ($this->options['access_control_allow_origin']) {
+ $this->send_access_control_headers();
+ }
+ $this->send_content_type_header();
+ }
+
+ public function get($print_response = true) {
+ if ($print_response && $this->get_query_param('download')) {
+ return $this->download();
+ }
+ $file_name = $this->get_file_name_param();
+ if ($file_name) {
+ $response = array(
+ $this->get_singular_param_name() => $this->get_file_object($file_name)
+ );
+ } else {
+ $response = array(
+ $this->options['param_name'] => $this->get_file_objects()
+ );
+ }
+ return $this->generate_response($response, $print_response);
+ }
+
+ public function post($print_response = true) {
+ if ($this->get_query_param('_method') === 'DELETE') {
+ return $this->delete($print_response);
+ }
+ $upload = $this->get_upload_data($this->options['param_name']);
+ // Parse the Content-Disposition header, if available:
+ $content_disposition_header = $this->get_server_var('HTTP_CONTENT_DISPOSITION');
+ $file_name = $content_disposition_header ?
+ rawurldecode(preg_replace(
+ '/(^[^"]+")|("$)/',
+ '',
+ $content_disposition_header
+ )) : null;
+ // Parse the Content-Range header, which has the following form:
+ // Content-Range: bytes 0-524287/2000000
+ $content_range_header = $this->get_server_var('HTTP_CONTENT_RANGE');
+ $content_range = $content_range_header ?
+ preg_split('/[^0-9]+/', $content_range_header) : null;
+ $size = @$content_range[3];
+ $files = array();
+ if ($upload) {
+ if (is_array($upload['tmp_name'])) {
+ // param_name is an array identifier like "files[]",
+ // $upload is a multi-dimensional array:
+ foreach ($upload['tmp_name'] as $index => $value) {
+ $files[] = $this->handle_file_upload(
+ $upload['tmp_name'][$index],
+ $file_name ? $file_name : $upload['name'][$index],
+ $size ? $size : $upload['size'][$index],
+ $upload['type'][$index],
+ $upload['error'][$index],
+ $index,
+ $content_range
+ );
+ }
+ } else {
+ // param_name is a single object identifier like "file",
+ // $upload is a one-dimensional array:
+ $files[] = $this->handle_file_upload(
+ isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
+ $file_name ? $file_name : (isset($upload['name']) ?
+ $upload['name'] : null),
+ $size ? $size : (isset($upload['size']) ?
+ $upload['size'] : $this->get_server_var('CONTENT_LENGTH')),
+ isset($upload['type']) ?
+ $upload['type'] : $this->get_server_var('CONTENT_TYPE'),
+ isset($upload['error']) ? $upload['error'] : null,
+ null,
+ $content_range
+ );
+ }
+ }
+ $response = array($this->options['param_name'] => $files);
+ return $this->generate_response($response, $print_response);
+ }
+
+ public function delete($print_response = true) {
+ $file_names = $this->get_file_names_params();
+ if (empty($file_names)) {
+ $file_names = array($this->get_file_name_param());
+ }
+ $response = array();
+ foreach ($file_names as $file_name) {
+ $file_path = $this->get_upload_path($file_name);
+ $success = strlen($file_name) > 0 && $file_name[0] !== '.' && is_file($file_path) && unlink($file_path);
+ if ($success) {
+ foreach ($this->options['image_versions'] as $version => $options) {
+ if (!empty($version)) {
+ $file = $this->get_upload_path($file_name, $version);
+ if (is_file($file)) {
+ unlink($file);
+ }
+ }
+ }
+ }
+ $response[$file_name] = $success;
+ }
+ return $this->generate_response($response, $print_response);
+ }
+
+ protected function basename($filepath, $suffix = null) {
+ $splited = preg_split('/\//', rtrim ($filepath, '/ '));
+ return substr(basename('X'.$splited[count($splited)-1], $suffix), 1);
+ }
+}
\ No newline at end of file