diff --git a/app/Config/Boot/development.php b/app/Config/Boot/development.php index 61707a9..7d6ae48 100644 --- a/app/Config/Boot/development.php +++ b/app/Config/Boot/development.php @@ -8,7 +8,7 @@ | painful debugging. */ error_reporting(-1); -ini_set('display_errors', 1); +ini_set('display_errors', '1'); /* |-------------------------------------------------------------------------- diff --git a/app/Config/Boot/production.php b/app/Config/Boot/production.php index 5f78a20..c54bdbd 100644 --- a/app/Config/Boot/production.php +++ b/app/Config/Boot/production.php @@ -7,7 +7,7 @@ | Don't show ANY in production environments. Instead, let the system catch | it and display a generic error message. */ -ini_set('display_errors', 0); +ini_set('display_errors', '0'); error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED); /* diff --git a/app/Config/Boot/testing.php b/app/Config/Boot/testing.php index fec4922..e6c94d7 100644 --- a/app/Config/Boot/testing.php +++ b/app/Config/Boot/testing.php @@ -9,7 +9,7 @@ | painful debugging. */ error_reporting(-1); -ini_set('display_errors', 1); +ini_set('display_errors', '1'); /* |-------------------------------------------------------------------------- diff --git a/app/Config/Constants.php b/app/Config/Constants.php index 3f7cef1..b25f71c 100644 --- a/app/Config/Constants.php +++ b/app/Config/Constants.php @@ -11,7 +11,7 @@ // NOTE: changing this will require manually modifying the // existing namespaces of App\* namespaced-classes. // -define('APP_NAMESPACE', 'App'); +defined('APP_NAMESPACE') || define('APP_NAMESPACE', 'App'); /* |-------------------------------------------------------------------------- @@ -21,7 +21,7 @@ | The path that Composer's autoload file is expected to live. By default, | the vendor folder is in the Root directory, but you can customize that here. */ -define('COMPOSER_PATH', ROOTPATH . 'vendor/autoload.php'); +defined('COMPOSER_PATH') || define('COMPOSER_PATH', ROOTPATH . 'vendor/autoload.php'); /* |-------------------------------------------------------------------------- diff --git a/app/Config/Paths.php b/app/Config/Paths.php index 5ef168c..6ca2d37 100644 --- a/app/Config/Paths.php +++ b/app/Config/Paths.php @@ -56,10 +56,6 @@ * --------------------------------------------------------------- * * This variable must contain the name of your "tests" directory. - * The writable directory allows you to group all directories that - * need write permission to a single place that can be tucked away - * for maximum security, keeping it out of the app and/or - * system directories. */ public $testsDirectory = __DIR__ . '/../../tests'; diff --git a/system/CLI/BaseCommand.php b/system/CLI/BaseCommand.php index 9358d4a..6013927 100644 --- a/system/CLI/BaseCommand.php +++ b/system/CLI/BaseCommand.php @@ -191,6 +191,20 @@ //-------------------------------------------------------------------- /** + * Makes it simple to check our protected properties. + * + * @param string $key + * + * @return bool + */ + public function __isset(string $key): bool + { + return isset($this->$key); + } + + //-------------------------------------------------------------------- + + /** * show Help include (usage,arguments,description,options) */ public function showHelp() diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 374a4bc..fb9a171 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -65,7 +65,7 @@ /** * The current version of CodeIgniter Framework */ - const CI_VERSION = '4.0.0-rc.1'; + const CI_VERSION = '4.0.0-rc.2'; /** * App startup time. diff --git a/system/Common.php b/system/Common.php index fa045d9..85f2a76 100644 --- a/system/Common.php +++ b/system/Common.php @@ -433,16 +433,16 @@ if (! function_exists('lang')) { /** - * A convenience method to translate a string and format it - * with the intl extension's MessageFormatter object. + * A convenience method to translate a string or array of them and format + * the result with the intl extension's MessageFormatter. * - * @param string $line - * @param array $args - * @param string $locale + * @param string|[] $line + * @param array $args + * @param string $locale * * @return string */ - function lang(string $line, array $args = [], string $locale = null): string + function lang(string $line, array $args = [], string $locale = null) { return Services::language($locale) ->getLine($line, $args); @@ -1079,7 +1079,22 @@ */ function dd(...$vars) { + Kint::$aliases[] = 'dd'; Kint::dump(...$vars); exit; } } + +//-------------------------------------------------------------------- + +if (! function_exists('trace')) +{ + /** + * Provides a backtrace to the current execution point, from Kint. + */ + function trace() + { + Kint::$aliases[] = 'trace'; + Kint::trace(); + } +} diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index ea3488d..e2ab08b 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -219,6 +219,13 @@ */ protected $canLimitWhereUpdates = true; + /** + * Builder testing mode status. + * + * @var boolean + */ + protected $testMode = false; + //-------------------------------------------------------------------- /** @@ -255,6 +262,22 @@ //-------------------------------------------------------------------- /** + * Sets a test mode status. + * + * @param boolean $mode Mode to set + * + * @return BaseBuilder + */ + public function testMode(bool $mode = true) + { + $this->testMode = $mode; + + return $this; + } + + //-------------------------------------------------------------------- + + /** * Returns an array of bind values and their * named parameters for binding in the Query object later. * @@ -830,6 +853,82 @@ //-------------------------------------------------------------------- /** + * HAVING IN + * + * Generates a HAVING field IN('item', 'item') SQL query, + * joined with 'AND' if appropriate. + * + * @param string $key The field to search + * @param array|string|Closure $values The values searched on, or anonymous function with subquery + * @param boolean $escape + * + * @return BaseBuilder + */ + public function havingIn(string $key = null, $values = null, bool $escape = null) + { + return $this->_whereIn($key, $values, false, 'AND ', $escape, 'QBHaving'); + } + + //-------------------------------------------------------------------- + + /** + * OR HAVING IN + * + * Generates a HAVING field IN('item', 'item') SQL query, + * joined with 'OR' if appropriate. + * + * @param string $key The field to search + * @param array|string|Closure $values The values searched on, or anonymous function with subquery + * @param boolean $escape + * + * @return BaseBuilder + */ + public function orHavingIn(string $key = null, $values = null, bool $escape = null) + { + return $this->_whereIn($key, $values, false, 'OR ', $escape, 'QBHaving'); + } + + //-------------------------------------------------------------------- + + /** + * HAVING NOT IN + * + * Generates a HAVING field NOT IN('item', 'item') SQL query, + * joined with 'AND' if appropriate. + * + * @param string $key The field to search + * @param array|string|Closure $values The values searched on, or anonymous function with subquery + * @param boolean $escape + * + * @return BaseBuilder + */ + public function havingNotIn(string $key = null, $values = null, bool $escape = null) + { + return $this->_whereIn($key, $values, true, 'AND ', $escape, 'QBHaving'); + } + + //-------------------------------------------------------------------- + + /** + * OR HAVING NOT IN + * + * Generates a HAVING field NOT IN('item', 'item') SQL query, + * joined with 'OR' if appropriate. + * + * @param string $key The field to search + * @param array|string|Closure $values The values searched on, or anonymous function with subquery + * @param boolean $escape + * + * @return BaseBuilder + */ + public function orHavingNotIn(string $key = null, $values = null, bool $escape = null) + { + return $this->_whereIn($key, $values, true, 'OR ', $escape, 'QBHaving'); + } + + //-------------------------------------------------------------------- + + /** * Internal WHERE IN * * @used-by WhereIn() @@ -842,10 +941,11 @@ * @param boolean $not If the statement would be IN or NOT IN * @param string $type * @param boolean $escape + * @param string $clause (Internal use only) * * @return BaseBuilder */ - protected function _whereIn(string $key = null, $values = null, bool $not = false, string $type = 'AND ', bool $escape = null) + protected function _whereIn(string $key = null, $values = null, bool $not = false, string $type = 'AND ', bool $escape = null, string $clause = 'QBWhere') { if ($key === null || $values === null || (! is_array($values) && ! ($values instanceof Closure))) { @@ -874,14 +974,14 @@ $ok = $this->setBind($ok, $whereIn, $escape); } - $prefix = empty($this->QBWhere) ? $this->groupGetType('') : $this->groupGetType($type); + $prefix = empty($this->$clause) ? $this->groupGetType('') : $this->groupGetType($type); $whereIn = [ 'condition' => $prefix . $key . $not . ($values instanceof Closure ? " IN ($ok)" : " IN :{$ok}:"), 'escape' => false, ]; - $this->QBWhere[] = $whereIn; + $this->{$clause}[] = $whereIn; return $this; } @@ -970,6 +1070,86 @@ return $this->_like($field, $match, 'OR ', $side, 'NOT', $escape, $insensitiveSearch); } + // -------------------------------------------------------------------- + + /** + * LIKE with HAVING clause + * + * Generates a %LIKE% portion of the query. + * Separates multiple calls with 'AND'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param boolean $escape + * + * @return BaseBuilder + */ + public function havingLike($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false) + { + return $this->_like($field, $match, 'AND ', $side, '', $escape, $insensitiveSearch, 'QBHaving'); + } + + // -------------------------------------------------------------------- + + /** + * NOT LIKE with HAVING clause + * + * Generates a NOT LIKE portion of the query. + * Separates multiple calls with 'AND'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param boolean $escape + * + * @return BaseBuilder + */ + public function notHavingLike($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false) + { + return $this->_like($field, $match, 'AND ', $side, 'NOT', $escape, $insensitiveSearch, 'QBHaving'); + } + + // -------------------------------------------------------------------- + + /** + * OR LIKE with HAVING clause + * + * Generates a %LIKE% portion of the query. + * Separates multiple calls with 'OR'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param boolean $escape + * + * @return BaseBuilder + */ + public function orHavingLike($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false) + { + return $this->_like($field, $match, 'OR ', $side, '', $escape, $insensitiveSearch, 'QBHaving'); + } + + // -------------------------------------------------------------------- + + /** + * OR NOT LIKE with HAVING clause + * + * Generates a NOT LIKE portion of the query. + * Separates multiple calls with 'OR'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param boolean $escape + * + * @return BaseBuilder + */ + public function orNotHavingLike($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false) + { + return $this->_like($field, $match, 'OR ', $side, 'NOT', $escape, $insensitiveSearch, 'QBHaving'); + } + //-------------------------------------------------------------------- /** @@ -979,6 +1159,10 @@ * @used-by orLike() * @used-by notLike() * @used-by orNotLike() + * @used-by havingLike() + * @used-by orHavingLike() + * @used-by notHavingLike() + * @used-by orNotHavingLike() * * @param mixed $field * @param string $match @@ -987,10 +1171,11 @@ * @param string $not * @param boolean $escape * @param boolean $insensitiveSearch IF true, will force a case-insensitive search + * @param string $clause (Internal use only) * * @return BaseBuilder */ - protected function _like($field, string $match = '', string $type = 'AND ', string $side = 'both', string $not = '', bool $escape = null, bool $insensitiveSearch = false) + protected function _like($field, string $match = '', string $type = 'AND ', string $side = 'both', string $not = '', bool $escape = null, bool $insensitiveSearch = false, string $clause = 'QBWhere') { if (! is_array($field)) { @@ -1004,13 +1189,13 @@ foreach ($field as $k => $v) { - $prefix = empty($this->QBWhere) ? $this->groupGetType('') : $this->groupGetType($type); - if ($insensitiveSearch === true) { $v = strtolower($v); } + $prefix = empty($this->$clause) ? $this->groupGetType('') : $this->groupGetType($type); + if ($side === 'none') { $bind = $this->setBind($k, $v, $escape); @@ -1036,7 +1221,7 @@ $like_statement .= sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar); } - $this->QBWhere[] = [ + $this->{$clause}[] = [ 'condition' => $like_statement, 'escape' => $escape, ]; @@ -1075,25 +1260,11 @@ /** * Starts a query group. * - * @param string $not (Internal use only) - * @param string $type (Internal use only) - * * @return BaseBuilder */ - public function groupStart(string $not = '', string $type = 'AND ') + public function groupStart() { - $type = $this->groupGetType($type); - - $this->QBWhereGroupStarted = true; - $prefix = empty($this->QBWhere) ? '' : $type; - $where = [ - 'condition' => $prefix . $not . str_repeat(' ', ++ $this->QBWhereGroupCount) . ' (', - 'escape' => false, - ]; - - $this->QBWhere[] = $where; - - return $this; + return $this->groupStartPrepare('', 'AND ', 'QBWhere'); } //-------------------------------------------------------------------- @@ -1105,7 +1276,7 @@ */ public function orGroupStart() { - return $this->groupStart('', 'OR '); + return $this->groupStartPrepare('', 'OR ', 'QBWhere'); } //-------------------------------------------------------------------- @@ -1117,7 +1288,7 @@ */ public function notGroupStart() { - return $this->groupStart('NOT ', 'AND '); + return $this->groupStartPrepare('NOT ', 'AND ', 'QBWhere'); } //-------------------------------------------------------------------- @@ -1129,7 +1300,7 @@ */ public function orNotGroupStart() { - return $this->groupStart('NOT ', 'OR '); + return $this->groupStartPrepare('NOT ', 'OR ', 'QBWhere'); } //-------------------------------------------------------------------- @@ -1141,13 +1312,114 @@ */ public function groupEnd() { + return $this->groupEndPrepare('QBWhere'); + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group for HAVING clause. + * + * @return BaseBuilder + */ + public function havingGroupStart() + { + return $this->groupStartPrepare('', 'AND ', 'QBHaving'); + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group for HAVING clause, but ORs the group. + * + * @return BaseBuilder + */ + public function orHavingGroupStart() + { + return $this->groupStartPrepare('', 'OR ', 'QBHaving'); + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group for HAVING clause, but NOTs the group. + * + * @return BaseBuilder + */ + public function notHavingGroupStart() + { + return $this->groupStartPrepare('NOT ', 'AND ', 'QBHaving'); + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group for HAVING clause, but OR NOTs the group. + * + * @return BaseBuilder + */ + public function orNotHavingGroupStart() + { + return $this->groupStartPrepare('NOT ', 'OR ', 'QBHaving'); + } + + // -------------------------------------------------------------------- + + /** + * Ends a query group for HAVING clause. + * + * @return BaseBuilder + */ + public function havingGroupEnd() + { + return $this->groupEndPrepare('QBHaving'); + } + + //-------------------------------------------------------------------- + + /** + * Prepate a query group start. + * + * @param string $not + * @param string $type + * @param string $clause + * + * @return BaseBuilder + */ + protected function groupStartPrepare(string $not = '', string $type = 'AND ', string $clause = 'QBWhere') + { + $type = $this->groupGetType($type); + + $this->QBWhereGroupStarted = true; + $prefix = empty($this->$clause) ? '' : $type; + $where = [ + 'condition' => $prefix . $not . str_repeat(' ', ++ $this->QBWhereGroupCount) . ' (', + 'escape' => false, + ]; + + $this->{$clause}[] = $where; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Prepate a query group end. + * + * @param string $clause + * + * @return BaseBuilder + */ + protected function groupEndPrepare(string $clause = 'QBWhere') + { $this->QBWhereGroupStarted = false; $where = [ 'condition' => str_repeat(' ', $this->QBWhereGroupCount -- ) . ')', 'escape' => false, ]; - $this->QBWhere[] = $where; + $this->{$clause}[] = $where; return $this; } @@ -1161,6 +1433,7 @@ * @used-by _like() * @used-by whereHaving() * @used-by _whereIn() + * @used-by havingGroupStart() * * @param string $type * @@ -1491,21 +1764,20 @@ * Compiles the select statement based on the other functions called * and runs the query * - * @param integer $limit The limit clause - * @param integer $offset The offset clause - * @param boolean $returnSQL If true, returns the generate SQL, otherwise executes the query. - * @param boolean $reset Are we want to clear query builder values? + * @param integer $limit The limit clause + * @param integer $offset The offset clause + * @param boolean $reset Are we want to clear query builder values? * * @return ResultInterface */ - public function get(int $limit = null, int $offset = 0, bool $returnSQL = false, bool $reset = true) + public function get(int $limit = null, int $offset = 0, bool $reset = true) { if (! is_null($limit)) { $this->limit($limit, $offset); } - $result = $returnSQL + $result = $this->testMode ? $this->getCompiledSelect($reset) : $this->db->query($this->compileSelect(), $this->binds, false); @@ -1529,18 +1801,17 @@ * the specified database * * @param boolean $reset Are we want to clear query builder values? - * @param boolean $test Are we running automated tests? * * @return integer|string when $test = true */ - public function countAll(bool $reset = true, bool $test = false) + public function countAll(bool $reset = true) { $table = $this->QBFrom[0]; $sql = $this->countString . $this->db->escapeIdentifiers('numrows') . ' FROM ' . $this->db->protectIdentifiers($table, true, null, false); - if ($test) + if ($this->testMode) { return $sql; } @@ -1570,11 +1841,10 @@ * returned by an Query Builder query. * * @param boolean $reset - * @param boolean $test The reset clause * * @return integer|string when $test = true */ - public function countAllResults(bool $reset = true, bool $test = false) + public function countAllResults(bool $reset = true) { // ORDER BY usage is often problematic here (most notably // on Microsoft SQL Server) and ultimately unnecessary @@ -1596,7 +1866,7 @@ : $this->compileSelect($this->countString . $this->db->protectIdentifiers('numrows')); - if ($test) + if ($this->testMode) { return $sql; } @@ -1647,13 +1917,14 @@ * * Allows the where clause, limit and offset to be added directly * - * @param string|array $where - * @param integer $limit - * @param integer $offset + * @param string|array $where Where condition + * @param integer $limit Limit value + * @param integer $offset Offset value + * @param boolean $reset Are we want to clear query builder values? * * @return ResultInterface */ - public function getWhere($where = null, int $limit = null, int $offset = null) + public function getWhere($where = null, int $limit = null, ?int $offset = 0, bool $reset = true) { if ($where !== null) { @@ -1665,8 +1936,17 @@ $this->limit($limit, $offset); } - $result = $this->db->query($this->compileSelect(), $this->binds, false); - $this->resetSelect(); + $result = $this->testMode + ? $this->getCompiledSelect($reset) + : $this->db->query($this->compileSelect(), $this->binds, false); + + if ($reset === true) + { + $this->resetSelect(); + + // Clear our binds so we don't eat up memory + $this->binds = []; + } return $result; } @@ -1680,14 +1960,12 @@ * * @param array $set An associative array of insert values * @param boolean $escape Whether to escape values and identifiers - * - * @param integer $batchSize - * @param boolean $testing + * @param integer $batchSize Batch size * * @return integer Number of rows inserted or FALSE on failure * @throws DatabaseException */ - public function insertBatch(array $set = null, bool $escape = null, int $batchSize = 100, bool $testing = false) + public function insertBatch(array $set = null, bool $escape = null, int $batchSize = 100) { if ($set === null) { @@ -1724,7 +2002,7 @@ { $sql = $this->_insertBatch($this->db->protectIdentifiers($table, true, $escape, false), $this->QBKeys, array_slice($this->QBSet, $i, $batchSize)); - if ($testing) + if ($this->testMode) { ++ $affected_rows; } @@ -1735,7 +2013,7 @@ } } - if (! $testing) + if (! $this->testMode) { $this->resetWrite(); } @@ -1859,11 +2137,10 @@ * * @param array $set An associative array of insert values * @param boolean $escape Whether to escape values and identifiers - * @param boolean $test Used when running tests * * @return BaseResult|Query|false */ - public function insert(array $set = null, bool $escape = null, bool $test = false) + public function insert(array $set = null, bool $escape = null) { if ($set !== null) { @@ -1881,7 +2158,7 @@ ), array_keys($this->QBSet), array_values($this->QBSet) ); - if ($test === false) + if (! $this->testMode) { $this->resetWrite(); @@ -1948,13 +2225,12 @@ * * Compiles an replace into string and runs the query * - * @param array $set An associative array of insert values - * @param boolean $returnSQL + * @param array $set An associative array of insert values * * @return BaseResult|Query|string|false * @throws DatabaseException */ - public function replace(array $set = null, bool $returnSQL = false) + public function replace(array $set = null) { if ($set !== null) { @@ -1976,7 +2252,7 @@ $this->resetWrite(); - return $returnSQL ? $sql : $this->db->query($sql, $this->binds, false); + return $this->testMode ? $sql : $this->db->query($sql, $this->binds, false); } //-------------------------------------------------------------------- @@ -2052,11 +2328,10 @@ * @param array $set An associative array of update values * @param mixed $where * @param integer $limit - * @param boolean $test Are we testing the code? * * @return boolean TRUE on success, FALSE on failure */ - public function update(array $set = null, $where = null, int $limit = null, bool $test = false): bool + public function update(array $set = null, $where = null, int $limit = null): bool { if ($set !== null) { @@ -2085,7 +2360,7 @@ $sql = $this->_update($this->QBFrom[0], $this->QBSet); - if (! $test) + if (! $this->testMode) { $this->resetWrite(); @@ -2167,12 +2442,11 @@ * @param array $set An associative array of update values * @param string $index The where key * @param integer $batchSize The size of the batch to run - * @param boolean $returnSQL True means SQL is returned, false will execute the query * * @return mixed Number of rows affected, SQL string, or FALSE on failure * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function updateBatch(array $set = null, string $index = null, int $batchSize = 100, bool $returnSQL = false) + public function updateBatch(array $set = null, string $index = null, int $batchSize = 100) { if ($index === null) { @@ -2219,7 +2493,7 @@ $sql = $this->_updateBatch($table, array_slice($this->QBSet, $i, $batchSize), $this->db->protectIdentifiers($index) ); - if ($returnSQL) + if ($this->testMode) { $savedSQL[] = $sql; } @@ -2234,7 +2508,7 @@ $this->resetWrite(); - return $returnSQL ? $savedSQL : $affected_rows; + return $this->testMode ? $savedSQL : $affected_rows; } //-------------------------------------------------------------------- @@ -2337,16 +2611,15 @@ * * Compiles a delete string and runs "DELETE FROM table" * - * @param boolean $test * @return boolean TRUE on success, FALSE on failure */ - public function emptyTable(bool $test = false) + public function emptyTable() { $table = $this->QBFrom[0]; $sql = $this->_delete($table); - if ($test) + if ($this->testMode) { return $sql; } @@ -2365,17 +2638,15 @@ * If the database does not support the truncate() command * This function maps to "DELETE FROM table" * - * @param boolean $test Whether we're in test mode or not. - * * @return boolean TRUE on success, FALSE on failure */ - public function truncate(bool $test = false) + public function truncate() { $table = $this->QBFrom[0]; $sql = $this->_truncate($table); - if ($test === true) + if ($this->testMode) { return $sql; } @@ -2434,12 +2705,11 @@ * @param mixed $where The where clause * @param integer $limit The limit clause * @param boolean $reset_data - * @param boolean $returnSQL * * @return mixed * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function delete($where = '', int $limit = null, bool $reset_data = true, bool $returnSQL = false) + public function delete($where = '', int $limit = null, bool $reset_data = true) { $table = $this->db->protectIdentifiers($this->QBFrom[0], true, null, false); @@ -2480,7 +2750,7 @@ $this->resetWrite(); } - return ($returnSQL === true) ? $sql : $this->db->query($sql, $this->binds, false); + return $this->testMode ? $sql : $this->db->query($sql, $this->binds, false); } //-------------------------------------------------------------------- diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 8bcbac9..6fd5a49 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -515,6 +515,34 @@ //-------------------------------------------------------------------- /** + * Set DB Prefix + * + * Set's the DB Prefix to something new without needing to reconnect + * + * @param string $prefix The prefix + * + * @return string + */ + public function setPrefix(string $prefix = ''): string + { + return $this->DBPrefix = $prefix; + } + + //-------------------------------------------------------------------- + + /** + * Returns the database prefix. + * + * @return string + */ + public function getPrefix(): string + { + return $this->DBPrefix; + } + + //-------------------------------------------------------------------- + + /** * Returns the last error encountered by this connection. * * @return mixed @@ -1330,22 +1358,6 @@ //-------------------------------------------------------------------- /** - * Set DB Prefix - * - * Set's the DB Prefix to something new without needing to reconnect - * - * @param string $prefix The prefix - * - * @return string - */ - public function setPrefix(string $prefix = ''): string - { - return $this->DBPrefix = $prefix; - } - - //-------------------------------------------------------------------- - - /** * Returns the total number of rows affected by this query. * * @return mixed @@ -1516,7 +1528,9 @@ // Is there a cached result? if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) { - return $this->dataCache['table_names']; + return $constrainByPrefix ? + preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names']) + : $this->dataCache['table_names']; } if (false === ($sql = $this->_listTables($constrainByPrefix))) @@ -1850,4 +1864,18 @@ //-------------------------------------------------------------------- + /** + * Checker for properties existence. + * + * @param string $key + * + * @return bool + */ + public function __isset(string $key): bool + { + return property_exists($this, $key); + } + + //-------------------------------------------------------------------- + } diff --git a/system/Database/Config.php b/system/Database/Config.php index 5588035..f275066 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -87,7 +87,7 @@ $group = 'custom-' . md5(json_encode($config)); } - $config = $config ?? new \Config\Database(); + $config = $config ?? config('Database'); if (empty($group)) { diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 6aae0d9..2907cd6 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -605,7 +605,7 @@ } // If the prefix is already starting the table name, remove it... - if (! empty($this->db->DBPrefix) && strpos($tableName, $this->db->DBPrefix) === 0) + if ($this->db->DBPrefix && strpos($tableName, $this->db->DBPrefix) === 0) { $tableName = substr($tableName, strlen($this->db->DBPrefix)); } diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index 76331c8..25fe423 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -298,7 +298,7 @@ } // Get the migrations from each history - foreach ($this->getBatchHistory($batch) as $history) + foreach ($this->getBatchHistory($batch, 'desc') as $history) { // Create a UID from the history to match its migration $uid = $this->getObjectUid($history); @@ -789,13 +789,13 @@ * * @return array */ - public function getBatchHistory(int $batch): array + public function getBatchHistory(int $batch, $order = 'asc'): array { $this->ensureTable(); $query = $this->db->table($this->table) ->where('batch', $batch) - ->orderBy('id', 'asc') + ->orderBy('id', $order) ->get(); return $query ? $query->getResultObject() : []; diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 4a455ff..059c860 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -390,7 +390,48 @@ //-------------------------------------------------------------------- /** + * Escape Like String Direct + * There are a few instances where MySQLi queries cannot take the + * additional "ESCAPE x" parameter for specifying the escape character + * in "LIKE" strings, and this handles those directly with a backslash. + * + * @param string|string[] $str Input string + * @return string|string[] + */ + public function escapeLikeStringDirect($str) + { + if (is_array($str)) + { + foreach ($str as $key => $val) + { + $str[$key] = $this->escapeLikeStringDirect($val); + } + + return $str; + } + + $str = $this->_escapeString($str); + + // Escape LIKE condition wildcards + return str_replace([ + $this->likeEscapeChar, + '%', + '_', + ], [ + '\\' . $this->likeEscapeChar, + '\\' . '%', + '\\' . '_', + ], $str + ); + + return $str; + } + + //-------------------------------------------------------------------- + + /** * Generates the SQL for listing tables in a platform-dependent manner. + * Uses escapeLikeStringDirect(). * * @param boolean $prefixLimit * @@ -402,7 +443,7 @@ if ($prefixLimit !== false && $this->DBPrefix !== '') { - return $sql . " LIKE '" . $this->escapeLikeString($this->DBPrefix) . "%'"; + return $sql . " LIKE '" . $this->escapeLikeStringDirect($this->DBPrefix) . "%'"; } return $sql; @@ -537,10 +578,14 @@ SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, - rc.REFERENCED_TABLE_NAME + kcu.COLUMN_NAME, + rc.REFERENCED_TABLE_NAME, + kcu.REFERENCED_COLUMN_NAME FROM information_schema.TABLE_CONSTRAINTS AS tc INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS AS rc ON tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + INNER JOIN information_schema.KEY_COLUMN_USAGE AS kcu + ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME WHERE tc.CONSTRAINT_TYPE = ' . $this->escape('FOREIGN KEY') . ' AND tc.TABLE_SCHEMA = ' . $this->escape($this->database) . ' AND @@ -555,10 +600,12 @@ $retVal = []; foreach ($query as $row) { - $obj = new \stdClass(); - $obj->constraint_name = $row->CONSTRAINT_NAME; - $obj->table_name = $row->TABLE_NAME; - $obj->foreign_table_name = $row->REFERENCED_TABLE_NAME; + $obj = new \stdClass(); + $obj->constraint_name = $row->CONSTRAINT_NAME; + $obj->table_name = $row->TABLE_NAME; + $obj->column_name = $row->COLUMN_NAME; + $obj->foreign_table_name = $row->REFERENCED_TABLE_NAME; + $obj->foreign_column_name = $row->REFERENCED_COLUMN_NAME; $retVal[] = $obj; } diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 9704c8c..e86f37c 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -137,14 +137,13 @@ * we simply do a DELETE and an INSERT on the first key/value * combo, assuming that it's either the primary key or a unique key. * - * @param array $set An associative array of insert values - * @param boolean $returnSQL + * @param array $set An associative array of insert values * * @return mixed * @throws DatabaseException * @internal param true $bool returns the generated SQL, false executes the query. */ - public function replace(array $set = null, bool $returnSQL = false) + public function replace(array $set = null) { if ($set !== null) { @@ -202,7 +201,6 @@ * @param mixed $where * @param integer $limit * @param boolean $reset_data - * @param boolean $returnSQL * * @return mixed * @throws DatabaseException @@ -210,14 +208,14 @@ * @internal param the $mixed limit clause * @internal param $bool */ - public function delete($where = '', int $limit = null, bool $reset_data = true, bool $returnSQL = false) + public function delete($where = '', int $limit = null, bool $reset_data = true) { if (! empty($limit) || ! empty($this->QBLimit)) { throw new DatabaseException('PostgreSQL does not allow LIMITs on DELETE queries.'); } - return parent::delete($where, $limit, $reset_data, $returnSQL); + return parent::delete($where, $limit, $reset_data); } //-------------------------------------------------------------------- diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 8becda2..be9556a 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -406,11 +406,13 @@ $retVal = []; foreach ($query as $row) { - $obj = new \stdClass(); - $obj->constraint_name = $row->constraint_name; - $obj->table_name = $row->table_name; - $obj->foreign_table_name = $row->foreign_table_name; - $retVal[] = $obj; + $obj = new \stdClass(); + $obj->constraint_name = $row->constraint_name; + $obj->table_name = $row->table_name; + $obj->column_name = $row->column_name; + $obj->foreign_table_name = $row->foreign_table_name; + $obj->foreign_column_name = $row->foreign_column_name; + $retVal[] = $obj; } return $retVal; diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index 576ebef..f03b6bb 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -207,6 +207,7 @@ protected function _listTables(bool $prefixLimit = false): string { return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\'' + . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\'' . (($prefixLimit !== false && $this->DBPrefix !== '') ? ' AND "NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . '%\' ' . sprintf($this->likeEscapeStr, $this->likeEscapeChar) @@ -407,6 +408,7 @@ $obj->constraint_name = $row->from . ' to ' . $row->table . '.' . $row->to; $obj->table_name = $table; $obj->foreign_table_name = $row->table; + $obj->sequence = $row->seq; $retVal[] = $obj; } diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 009a081..6e6b33a 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -1,5 +1,4 @@ viewDirectory . '/errors/'; } - $path = is_cli() - ? $path . 'cli/' - : $path . 'html/'; + $path = is_cli() ? $path . 'cli/' : $path . 'html/'; // Determine the vew $view = $this->determineView($exception, $path); @@ -463,7 +461,7 @@ $source = array_splice($source, $start, $lines, true); // Used to format the line number in the source - $format = '% ' . strlen($start + $lines) . 'd'; + $format = '% ' . strlen(sprintf('%sd', $start + $lines)); $out = ''; // Because the highlighting may have an uneven number diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index c9889d0..4adfd95 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -195,4 +195,15 @@ return null; } + /** + * __isset() magic, providing checking for some of our protected properties + * + * @param string $key Property name + * @return bool + */ + public function __isset($key): bool + { + return in_array($key, ['key', 'digest', 'driver', 'drivers'], true); + } + } diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index ad2568e..3a20644 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -110,4 +110,14 @@ return null; } + /** + * __isset() magic, providing checking for some of our properties + * + * @param string $key Property name + * @return bool + */ + public function __isset($key): bool + { + return in_array($key, ['cipher', 'key'], true); + } } diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index b0ca8b7..abeb300 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -672,10 +672,10 @@ } // Debug - if (isset($config['debug'])) + if ($config['debug']) { - $curl_options[CURLOPT_VERBOSE] = $config['debug'] === true ? 1 : 0; - $curl_options[CURLOPT_STDERR] = $config['debug'] === true ? fopen('php://output', 'w+') : $config['debug']; + $curl_options[CURLOPT_VERBOSE] = 1; + $curl_options[CURLOPT_STDERR] = is_string($config['debug']) ? fopen($config['debug'], 'a+') : fopen('php://output', 'w+'); } // Decode Content diff --git a/system/HTTP/Files/FileCollection.php b/system/HTTP/Files/FileCollection.php index df544ad..4481f8e 100644 --- a/system/HTTP/Files/FileCollection.php +++ b/system/HTTP/Files/FileCollection.php @@ -112,6 +112,41 @@ //-------------------------------------------------------------------- /** + * Verify if a file exist in the collection of uploaded files and is have been uploaded with multiple option. + * + * @param string $name + * + * @return array|null + */ + public function getFileMultiple(string $name) + { + $this->populateFiles(); + + if ($this->hasFile($name)) + { + if (strpos($name, '.') !== false) + { + $name = explode('.', $name); + $uploadedFile = $this->getValueDotNotationSyntax($name, $this->files); + + return (is_array($uploadedFile) && ($uploadedFile[0] instanceof UploadedFile)) ? + $uploadedFile : null; + } + + if (array_key_exists($name, $this->files)) + { + $uploadedFile = $this->files[$name]; + return (is_array($uploadedFile) && ($uploadedFile[0] instanceof UploadedFile)) ? + $uploadedFile : null; + } + } + + return null; + } + + //-------------------------------------------------------------------- + + /** * Checks whether an uploaded file with name $fileID exists in * this request. * diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 185b3bb..e064e6f 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -548,6 +548,26 @@ //-------------------------------------------------------------------- /** + * Verify if a file exist, by the name of the input field used to upload it, in the collection + * of uploaded files and if is have been uploaded with multiple option. + * + * @param string $fileID + * + * @return array|null + */ + public function getFileMultiple(string $fileID) + { + if (is_null($this->files)) + { + $this->files = new FileCollection(); + } + + return $this->files->getFileMultiple($fileID); + } + + //-------------------------------------------------------------------- + + /** * Retrieves a single file by the name of the input field used * to upload it. * diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index 7e64f52..1ddb686 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -234,7 +234,7 @@ */ public function isValidIP(string $ip = null, string $which = null): bool { - switch (strtolower($which)) + switch (strtolower( (string) $which)) { case 'ipv4': $which = FILTER_FLAG_IPV4; diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index d9c02f5..23855c2 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -714,7 +714,7 @@ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html if (! isset($this->headers['Date'])) { - $this->setDate(\DateTime::createFromFormat('U', time())); + $this->setDate(\DateTime::createFromFormat('U', (string) time())); } // HTTP Status diff --git a/system/I18n/Time.php b/system/I18n/Time.php index 92fd485..d56f0cf 100644 --- a/system/I18n/Time.php +++ b/system/I18n/Time.php @@ -1342,4 +1342,20 @@ return null; } + //-------------------------------------------------------------------- + + /** + * Allow for property-type checking to any getX method... + * + * @param $name + * + * @return bool + */ + public function __isset($name): bool + { + $method = 'get' . ucfirst($name); + + return method_exists($this, $method); + } + } diff --git a/system/I18n/TimeDifference.php b/system/I18n/TimeDifference.php index e431213..9af8269 100644 --- a/system/I18n/TimeDifference.php +++ b/system/I18n/TimeDifference.php @@ -345,4 +345,19 @@ return null; } + + /** + * Allow property-like checking for our calculated values. + * + * @param $name + * + * @return bool + */ + public function __isset($name) + { + $name = ucfirst(strtolower($name)); + $method = "get{$name}"; + + return method_exists($this, $method); + } } diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index cc5c136..6c5d198 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -1,5 +1,4 @@ image->getPathname()); - if (! is_null($key) && is_array($exif)) + $exif = null; // default + switch ($this->image->imageType) { - $exif = $exif[$key] ?? false; + case IMAGETYPE_JPEG: + case IMAGETYPE_TIFF_II: + $exif = exif_read_data($this->image->getPathname()); + if (! is_null($key) && is_array($exif)) + { + $exif = $exif[$key] ?? false; + } } return $exif; @@ -800,6 +813,7 @@ } //-------------------------------------------------------------------- + /** * Return image width. * diff --git a/system/Model.php b/system/Model.php index 8705276..60bd3bb 100644 --- a/system/Model.php +++ b/system/Model.php @@ -1634,6 +1634,31 @@ return null; } + /** + * Checks for the existence of properties across this model, builder, and db connection. + * + * @param string $name + * + * @return bool + */ + public function __isset(string $name): bool + { + if (property_exists($this, $name)) + { + return true; + } + elseif (isset($this->db->$name)) + { + return true; + } + elseif (isset($this->builder()->$name)) + { + return true; + } + + return false; + } + //-------------------------------------------------------------------- /** diff --git a/system/Pager/PagerRenderer.php b/system/Pager/PagerRenderer.php index a532c36..e7ce557 100644 --- a/system/Pager/PagerRenderer.php +++ b/system/Pager/PagerRenderer.php @@ -81,7 +81,7 @@ */ protected $pageCount; /** - * URI? unused? + * URI base for pagination links * * @var integer */ diff --git a/system/RESTful/ResourcePresenter.php b/system/RESTful/ResourcePresenter.php index ce4c7d1..d8e14f6 100644 --- a/system/RESTful/ResourcePresenter.php +++ b/system/RESTful/ResourcePresenter.php @@ -178,9 +178,11 @@ if (is_object($which)) { $this->model = $which; + $this->modelName = null; } else { + $this->model = null; $this->modelName = $which; } } diff --git a/system/Security/Security.php b/system/Security/Security.php index 0468ce0..a53525c 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -207,8 +207,14 @@ return $this->CSRFSetCookie($request); } - // Do the tokens exist in both the _POST and _COOKIE arrays? - if (! isset($_POST[$this->CSRFTokenName], $_COOKIE[$this->CSRFCookieName]) || $_POST[$this->CSRFTokenName] !== $_COOKIE[$this->CSRFCookieName] + // Do the token exist in _POST or php://input (json) data? + $CSRFTokenValue = $_POST[$this->CSRFTokenName] ?? + (!empty($input = file_get_contents('php://input')) && !empty($json = json_decode($input)) && json_last_error() === JSON_ERROR_NONE ? + ($json->{$this->CSRFTokenName} ?? null) : + null); + + // Do the tokens exist in both the _POST/POSTed JSON and _COOKIE arrays? + if (! isset($CSRFTokenValue, $_COOKIE[$this->CSRFCookieName]) || $CSRFTokenValue !== $_COOKIE[$this->CSRFCookieName] ) // Do the tokens match? { throw SecurityException::forDisallowedAction(); diff --git a/system/Session/Session.php b/system/Session/Session.php index 08b3c05..80c87ea 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -304,7 +304,8 @@ $this->sessionExpiration, $this->cookiePath, $this->cookieDomain, $this->cookieSecure, true // HTTP only; Yes, this is intentional and not configurable for security reasons. ); - if (empty($this->sessionExpiration)) + //if (empty($this->sessionExpiration)) + if (!isset($this->sessionExpiration)) { $this->sessionExpiration = (int) ini_get('session.gc_maxlifetime'); } @@ -625,6 +626,22 @@ } //-------------------------------------------------------------------- + + /** + * Magic method to check for session variables. + * Different from has() in that it will validate 'session_id' as well. + * Mostly used by internal PHP functions, users should stick to has() + * + * @param string $key Identifier of the session property to remove. + * + * @return bool + */ + public function __isset(string $key): bool + { + return isset($_SESSION[$key]) || ($key === 'session_id'); + } + + //-------------------------------------------------------------------- //-------------------------------------------------------------------- // Flash Data Methods //-------------------------------------------------------------------- diff --git a/system/Test/CIDatabaseTestCase.php b/system/Test/CIDatabaseTestCase.php index aeedb8e..f63ca72 100644 --- a/system/Test/CIDatabaseTestCase.php +++ b/system/Test/CIDatabaseTestCase.php @@ -158,7 +158,7 @@ * * @throws ConfigException */ - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -173,6 +173,7 @@ { $this->migrations->setNamespace($this->namespace); } + $this->migrations->regress(0, 'tests'); // Delete all of the tables to ensure we're at a clean start. $tables = $this->db->listTables(); @@ -192,7 +193,6 @@ } } - $this->migrations->regress(0, 'tests'); $this->migrations->latest('tests'); } @@ -200,7 +200,7 @@ { if (! empty($this->basePath)) { - $this->seeder->setPath(rtrim($this->basePath, '/') . '/seeds'); + $this->seeder->setPath(rtrim($this->basePath, '/') . '/Seeds'); } $this->seed($this->seed); @@ -213,7 +213,7 @@ * Takes care of any required cleanup after the test, like * removing any rows inserted via $this->hasInDatabase() */ - public function tearDown() + public function tearDown(): void { if (! empty($this->insertCache)) { diff --git a/system/Test/CIUnitTestCase.php b/system/Test/CIUnitTestCase.php index ce703e8..b584824 100644 --- a/system/Test/CIUnitTestCase.php +++ b/system/Test/CIUnitTestCase.php @@ -56,7 +56,7 @@ */ protected $app; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/system/Validation/FileRules.php b/system/Validation/FileRules.php index 18a5f08..4109ae3 100644 --- a/system/Validation/FileRules.php +++ b/system/Validation/FileRules.php @@ -120,13 +120,25 @@ $params = explode(',', $params); $name = array_shift($params); - $file = $this->request->getFile($name); - - if (is_null($file)) + if(!($files = $this->request->getFileMultiple($name))) { - return false; + $files = [$this->request->getFile($name)]; } - return $params[0] >= $file->getSize() / 1024; + + foreach ($files as $file) + { + if (is_null($file)) + { + return false; + } + + if ($file->getSize() / 1024 > $params[0]) + { + return false; + } + } + + return true; } //-------------------------------------------------------------------- @@ -148,18 +160,29 @@ $params = explode(',', $params); $name = array_shift($params); - $file = $this->request->getFile($name); - - if (is_null($file)) + if(!($files = $this->request->getFileMultiple($name))) { - return false; + $files = [$this->request->getFile($name)]; } - // We know that our mimes list always has the first mime - // start with `image` even when then are multiple accepted types. - $type = \Config\Mimes::guessTypeFromExtension($file->getExtension()); + foreach ($files as $file) + { + if (is_null($file)) + { + return false; + } - return mb_strpos($type, 'image') === 0; + // We know that our mimes list always has the first mime + // start with `image` even when then are multiple accepted types. + $type = \Config\Mimes::guessTypeFromExtension($file->getExtension()); + + if (mb_strpos($type, 'image') !== 0) + { + return false; + } + } + + return true; } //-------------------------------------------------------------------- @@ -180,14 +203,25 @@ $params = explode(',', $params); $name = array_shift($params); - $file = $this->request->getFile($name); - - if (is_null($file)) + if(!($files = $this->request->getFileMultiple($name))) { - return false; + $files = [$this->request->getFile($name)]; } - return in_array($file->getMimeType(), $params); + foreach ($files as $file) + { + if (is_null($file)) + { + return false; + } + + if (!in_array($file->getMimeType(), $params)) + { + return false; + } + } + + return true; } //-------------------------------------------------------------------- @@ -208,14 +242,25 @@ $params = explode(',', $params); $name = array_shift($params); - $file = $this->request->getFile($name); - - if (is_null($file)) + if(!($files = $this->request->getFileMultiple($name))) { - return false; + $files = [$this->request->getFile($name)]; } - return in_array($file->getExtension(), $params); + foreach ($files as $file) + { + if (is_null($file)) + { + return false; + } + + if (!in_array($file->getExtension(), $params)) + { + return false; + } + } + + return true; } //-------------------------------------------------------------------- @@ -237,23 +282,34 @@ $params = explode(',', $params); $name = array_shift($params); - $file = $this->request->getFile($name); - - if (is_null($file)) + if(!($files = $this->request->getFileMultiple($name))) { - return false; + $files = [$this->request->getFile($name)]; } - // Get Parameter sizes - $allowedWidth = $params[0] ?? 0; - $allowedHeight = $params[1] ?? 0; + foreach ($files as $file) + { + if (is_null($file)) + { + return false; + } - // Get uploaded image size - $info = getimagesize($file->getTempName()); - $fileWidth = $info[0]; - $fileHeight = $info[1]; + // Get Parameter sizes + $allowedWidth = $params[0] ?? 0; + $allowedHeight = $params[1] ?? 0; - return $fileWidth <= $allowedWidth && $fileHeight <= $allowedHeight; + // Get uploaded image size + $info = getimagesize($file->getTempName()); + $fileWidth = $info[0]; + $fileHeight = $info[1]; + + if ( $fileWidth > $allowedWidth || $fileHeight > $allowedHeight) + { + return false; + } + } + + return true; } //-------------------------------------------------------------------- diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 2a8b361..2ffaf5d 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -1,5 +1,4 @@ $rule($value, $error) - : $set->$rule($value, $param, $data, $error); + $passed = $param === false ? $set->$rule($value, $error) : $set->$rule($value, $param, $data, $error); break; } @@ -291,8 +290,7 @@ // Set the error message if we didn't survive. if ($passed === false) { - $this->errors[$field] = is_null($error) ? $this->getErrorMessage($rule, $field, $label, $param) - : $error; + $this->errors[$field] = is_null($error) ? $this->getErrorMessage($rule, $field, $label, $param) : $error; return false; } @@ -493,7 +491,7 @@ } return $this->view->setVar('errors', $this->getErrors()) - ->render($this->config->templates[$template]); + ->render($this->config->templates[$template]); } //-------------------------------------------------------------------- @@ -519,7 +517,7 @@ } return $this->view->setVar('error', $this->getError($field)) - ->render($this->config->templates[$template]); + ->render($this->config->templates[$template]); } //-------------------------------------------------------------------- @@ -648,13 +646,7 @@ // passed along from a redirect_with_input request. if (empty($this->errors) && ! is_cli()) { - // Start up the session if it's not already - if (! isset($_SESSION)) - { - session(); - } - - if ($errors = session('_ci_validation_errors')) + if (isset($_SESSION) && session('_ci_validation_errors')) { $this->errors = unserialize($errors); } @@ -724,15 +716,15 @@ { $non_escape_bracket = '((?config->saveData; } - $view = str_replace('.php', '', $view); + $fileExt = pathinfo($view, PATHINFO_EXTENSION); + $view = empty($fileExt) ? $view . '.php' : $view; // allow Views as .html, .tpl, etc (from CI3) // Was it cached? if (isset($options['cache'])) { - $cacheName = $options['cache_name'] ?: $view; + $cacheName = $options['cache_name'] ?? str_replace('.php', '', $view); if ($output = cache($cacheName)) { @@ -142,7 +143,6 @@ } } - $view = $view . '.php'; $file = $this->viewPath . $view; if (! is_file($file))