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