diff --git a/composer.json b/composer.json index 95ea6080..b2a1d6d0 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", - "kint-php/kint": "^4.1.1", + "kint-php/kint": "^4.2", "laminas/laminas-escaper": "^2.9", "psr/log": "^1.1" }, @@ -24,7 +24,18 @@ "predis/predis": "^1.1 || ^2.0" }, "suggest": { - "ext-fileinfo": "Improves mime type detection for files" + "ext-imagick": "If you use Image class ImageMagickHandler", + "ext-simplexml": "If you format XML", + "ext-mysqli": "If you use MySQL", + "ext-oci8": "If you use Oracle Database", + "ext-pgsql": "If you use PostgreSQL", + "ext-sqlsrv": "If you use SQL Server", + "ext-sqlite3": "If you use SQLite3", + "ext-memcache": "If you use Cache class MemcachedHandler with Memcache", + "ext-memcached": "If you use Cache class MemcachedHandler with Memcached", + "ext-redis": "If you use Cache class RedisHandler", + "ext-fileinfo": "Improves mime type detection for files", + "ext-readline": "Improves CLI::input() usability" }, "autoload": { "psr-4": { diff --git a/env b/env index cc0681c6..9dede2ea 100644 --- a/env +++ b/env @@ -48,7 +48,7 @@ # database.default.port = 3306 # database.tests.hostname = localhost -# database.tests.database = ci4 +# database.tests.database = ci4_test # database.tests.username = root # database.tests.password = root # database.tests.DBDriver = MySQLi diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 727fea4c..d4cc2944 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -47,7 +47,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.2.4'; + public const CI_VERSION = '4.2.5'; /** * App startup time. @@ -982,8 +982,10 @@ protected function gatherOutput(?Cache $cacheConfig = null, $returned = null) if ($returned instanceof DownloadResponse) { // Turn off output buffering completely, even if php.ini output_buffering is not off - while (ob_get_level() > 0) { - ob_end_clean(); + if (ENVIRONMENT !== 'testing') { + while (ob_get_level() > 0) { + ob_end_clean(); + } } $this->response = $returned; diff --git a/system/Commands/Cache/ClearCache.php b/system/Commands/Cache/ClearCache.php index bb98177a..648d99e4 100644 --- a/system/Commands/Cache/ClearCache.php +++ b/system/Commands/Cache/ClearCache.php @@ -46,7 +46,7 @@ class ClearCache extends BaseCommand * * @var string */ - protected $usage = 'cache:clear [driver]'; + protected $usage = 'cache:clear []'; /** * the Command's Arguments diff --git a/system/Commands/Help.php b/system/Commands/Help.php index 4dbc2df6..74dcbeab 100644 --- a/system/Commands/Help.php +++ b/system/Commands/Help.php @@ -48,7 +48,7 @@ class Help extends BaseCommand * * @var string */ - protected $usage = 'help command_name'; + protected $usage = 'help []'; /** * the Command's Arguments diff --git a/system/Common.php b/system/Common.php index 962483bb..be13c36f 100644 --- a/system/Common.php +++ b/system/Common.php @@ -19,6 +19,7 @@ use CodeIgniter\Debug\Timer; use CodeIgniter\Files\Exceptions\FileNotFoundException; use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; @@ -487,6 +488,10 @@ function force_https(int $duration = 31_536_000, ?RequestInterface $request = nu $response = Services::response(null, true); } + if (! $request instanceof IncomingRequest) { + return; + } + if ((ENVIRONMENT !== 'testing' && (is_cli() || $request->isSecure())) || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'test')) { // @codeCoverageIgnoreStart return; diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index f18417c5..39e832d1 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -577,7 +577,7 @@ public function fromSubquery(BaseBuilder $from, string $alias): self { $table = $this->buildSubquery($from, true, $alias); - $this->trackAliases($table); + $this->db->addTableAlias($alias); $this->QBFrom[] = $table; return $this; @@ -2705,8 +2705,7 @@ protected function objectToArray($object) $array = []; foreach (get_object_vars($object) as $key => $val) { - // There are some built in keys we need to ignore for this conversion - if (! is_object($val) && ! is_array($val) && $key !== '_parent_name') { + if (! is_object($val) && ! is_array($val)) { $array[$key] = $val; } } @@ -2732,13 +2731,10 @@ protected function batchObjectToArray($object) $fields = array_keys($out); foreach ($fields as $val) { - // There are some built in keys we need to ignore for this conversion - if ($val !== '_parent_name') { - $i = 0; + $i = 0; - foreach ($out[$val] as $data) { - $array[$i++][$val] = $data; - } + foreach ($out[$val] as $data) { + $array[$i++][$val] = $data; } } @@ -2952,7 +2948,7 @@ protected function buildSubquery($builder, bool $wrapped = false, string $alias throw new DatabaseException('The subquery cannot be the same object as the main query object.'); } - $subquery = strtr($builder->getCompiledSelect(), "\n", ' '); + $subquery = strtr($builder->getCompiledSelect(false), "\n", ' '); if ($wrapped) { $subquery = '(' . $subquery . ')'; diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 413c0a47..865a1893 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -562,7 +562,7 @@ public function addTableAlias(string $table) /** * Executes the query against the database. * - * @return mixed + * @return bool|object|resource */ abstract protected function execute(string $sql); @@ -882,7 +882,12 @@ public function table($tableName) */ public function newQuery(): BaseBuilder { - return $this->table(',')->from([], true); + // save table aliases + $tempAliases = $this->aliasedTables; + $builder = $this->table(',')->from([], true); + $this->aliasedTables = $tempAliases; + + return $builder; } /** @@ -1405,10 +1410,41 @@ public function listTables(bool $constrainByPrefix = false) /** * Determine if a particular table exists + * + * @param bool $cached Whether to use data cache */ - public function tableExists(string $tableName): bool + public function tableExists(string $tableName, bool $cached = true): bool { - return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables(), true); + if ($cached === true) { + return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables(), true); + } + + if (false === ($sql = $this->_listTables(false, $tableName))) { + if ($this->DBDebug) { + throw new DatabaseException('This feature is not available for the database you are using.'); + } + + return false; + } + + $tableExists = $this->query($sql)->getResultArray() !== []; + + // if cache has been built already + if (! empty($this->dataCache['table_names'])) { + $key = array_search( + strtolower($tableName), + array_map('strtolower', $this->dataCache['table_names']), + true + ); + + // table doesn't exist but still in cache - lets reset cache, it can be rebuilt later + // OR if table does exist but is not found in cache + if (($key !== false && ! $tableExists) || ($key === false && $tableExists)) { + $this->resetDataCache(); + } + } + + return $tableExists; } /** @@ -1575,9 +1611,11 @@ abstract public function insertID(); /** * Generates the SQL for listing tables in a platform-dependent manner. * + * @param string|null $tableName If $tableName is provided will return only this table if exists. + * * @return false|string */ - abstract protected function _listTables(bool $constrainByPrefix = false); + abstract protected function _listTables(bool $constrainByPrefix = false, ?string $tableName = null); /** * Generates a platform-specific query string so that the column names can be fetched. diff --git a/system/Database/Forge.php b/system/Database/Forge.php index d46dfdfd..5c13d27f 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -498,7 +498,7 @@ public function createTable(string $table, bool $ifNotExists = false, array $att } // If table exists lets stop here - if ($ifNotExists === true && $this->db->tableExists($table)) { + if ($ifNotExists === true && $this->db->tableExists($table, false)) { $this->reset(); return true; @@ -776,7 +776,7 @@ public function modifyColumn(string $table, $field): bool } /** - * @param mixed $fields + * @param array|string $fields * * @return false|string|string[] */ diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index e4fd5b3f..23a45251 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -277,7 +277,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @return mixed + * @return bool|object */ protected function execute(string $sql) { @@ -368,11 +368,17 @@ public function escapeLikeStringDirect($str) /** * Generates the SQL for listing tables in a platform-dependent manner. * Uses escapeLikeStringDirect(). + * + * @param string|null $tableName If $tableName is provided will return only this table if exists. */ - protected function _listTables(bool $prefixLimit = false): string + protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string { $sql = 'SHOW TABLES FROM ' . $this->escapeIdentifiers($this->database); + if ($tableName !== null) { + return $sql . ' LIKE ' . $this->escape($tableName); + } + if ($prefixLimit !== false && $this->DBPrefix !== '') { return $sql . " LIKE '" . $this->escapeLikeStringDirect($this->DBPrefix) . "%'"; } diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php index d00c26dd..0bee007c 100644 --- a/system/Database/MySQLi/Forge.php +++ b/system/Database/MySQLi/Forge.php @@ -128,9 +128,9 @@ protected function _createTableAttributes(array $attributes): string /** * ALTER TABLE * - * @param string $alterType ALTER type - * @param string $table Table name - * @param mixed $field Column definition + * @param string $alterType ALTER type + * @param string $table Table name + * @param array|string $field Column definition * * @return string|string[] */ diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index b84111fb..1cb9eb08 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -184,7 +184,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @return false|resource + * @return bool */ protected function execute(string $sql) { @@ -242,11 +242,17 @@ public function affectedRows(): int /** * Generates the SQL for listing tables in a platform-dependent manner. + * + * @param string|null $tableName If $tableName is provided will return only this table if exists. */ - protected function _listTables(bool $prefixLimit = false): string + protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string { $sql = 'SELECT "TABLE_NAME" FROM "USER_TABLES"'; + if ($tableName !== null) { + return $sql . ' WHERE "TABLE_NAME" LIKE ' . $this->escape($tableName); + } + if ($prefixLimit !== false && $this->DBPrefix !== '') { return $sql . ' WHERE "TABLE_NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . "%' " . sprintf($this->likeEscapeStr, $this->likeEscapeChar); diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php index 42393cd6..add01540 100644 --- a/system/Database/OCI8/Forge.php +++ b/system/Database/OCI8/Forge.php @@ -86,9 +86,9 @@ class Forge extends BaseForge /** * ALTER TABLE * - * @param string $alterType ALTER type - * @param string $table Table name - * @param mixed $field Column definition + * @param string $alterType ALTER type + * @param string $table Table name + * @param array|string $field Column definition * * @return string|string[] */ diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 7b58bdb2..4452eae3 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -132,7 +132,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @return mixed + * @return false|resource */ protected function execute(string $sql) { @@ -204,11 +204,17 @@ protected function _escapeString(string $str): string /** * Generates the SQL for listing tables in a platform-dependent manner. + * + * @param string|null $tableName If $tableName is provided will return only this table if exists. */ - protected function _listTables(bool $prefixLimit = false): string + protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string { $sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \'' . $this->schema . "'"; + if ($tableName !== null) { + return $sql . ' AND "table_name" LIKE ' . $this->escape($tableName); + } + if ($prefixLimit !== false && $this->DBPrefix !== '') { return $sql . ' AND "table_name" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . "%' " diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php index a050c688..4f7c75ea 100644 --- a/system/Database/Postgre/Forge.php +++ b/system/Database/Postgre/Forge.php @@ -76,7 +76,7 @@ protected function _createTableAttributes(array $attributes): string } /** - * @param mixed $field + * @param array|string $field * * @return array|bool|string */ diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index f180ca4f..74398fd2 100755 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -183,14 +183,20 @@ public function insertID(): int /** * Generates the SQL for listing tables in a platform-dependent manner. + * + * @param string|null $tableName If $tableName is provided will return only this table if exists. */ - protected function _listTables(bool $prefixLimit = false): string + protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string { $sql = 'SELECT [TABLE_NAME] AS "name"' . ' FROM [INFORMATION_SCHEMA].[TABLES] ' . ' WHERE ' . " [TABLE_SCHEMA] = '" . $this->schema . "' "; + if ($tableName !== null) { + return $sql .= ' AND [TABLE_NAME] LIKE ' . $this->escape($tableName); + } + if ($prefixLimit === true && $this->DBPrefix !== '') { $sql .= " AND [TABLE_NAME] LIKE '" . $this->escapeLikeString($this->DBPrefix) . "%' " . sprintf($this->likeEscapeStr, $this->likeEscapeChar); @@ -445,7 +451,7 @@ public function setDatabase(?string $databaseName = null) /** * Executes the query against the database. * - * @return mixed + * @return false|resource */ protected function execute(string $sql) { diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 95ec9ff0..bb802149 100755 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -119,7 +119,7 @@ protected function _createTableAttributes(array $attributes): string } /** - * @param mixed $field + * @param array|string $field * * @return false|string|string[] */ diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index 1085b30b..5ceef09a 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -16,6 +16,7 @@ use ErrorException; use Exception; use SQLite3; +use SQLite3Result; use stdClass; /** @@ -120,7 +121,7 @@ public function getVersion(): string /** * Execute the query * - * @return mixed \SQLite3Result object or bool + * @return bool|SQLite3Result */ protected function execute(string $sql) { @@ -160,9 +161,17 @@ protected function _escapeString(string $str): string /** * Generates the SQL for listing tables in a platform-dependent manner. + * + * @param string|null $tableName If $tableName is provided will return only this table if exists. */ - protected function _listTables(bool $prefixLimit = false): string + protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string { + if ($tableName !== null) { + return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\'' + . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\'' + . ' AND "NAME" LIKE ' . $this->escape($tableName); + } + return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\'' . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\'' . (($prefixLimit !== false && $this->DBPrefix !== '') diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index 154c9f97..9175dd2b 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -109,7 +109,7 @@ public function dropDatabase(string $dbName): bool } /** - * @param mixed $field + * @param array|string $field * * @return array|string|null */ diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php index 1ecb28fd..fbb8d0d0 100644 --- a/system/Database/SQLite3/Table.php +++ b/system/Database/SQLite3/Table.php @@ -240,6 +240,13 @@ protected function createTable() $this->forge->addField($fields); + $fieldNames = array_keys($fields); + + $this->keys = array_filter( + $this->keys, + static fn ($index) => count(array_intersect($index['fields'], $fieldNames)) === count($index['fields']) + ); + // Unique/Index keys if (is_array($this->keys)) { foreach ($this->keys as $key) { diff --git a/system/Email/Email.php b/system/Email/Email.php index 55c9c95e..3ef459a8 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -288,6 +288,13 @@ class Email */ protected $debugMessage = []; + /** + * Raw debug messages + * + * @var string[] + */ + private array $debugMessageRaw = []; + /** * Recipients * @@ -434,16 +441,17 @@ public function initialize($config) */ public function clear($clearAttachments = false) { - $this->subject = ''; - $this->body = ''; - $this->finalBody = ''; - $this->headerStr = ''; - $this->replyToFlag = false; - $this->recipients = []; - $this->CCArray = []; - $this->BCCArray = []; - $this->headers = []; - $this->debugMessage = []; + $this->subject = ''; + $this->body = ''; + $this->finalBody = ''; + $this->headerStr = ''; + $this->replyToFlag = false; + $this->recipients = []; + $this->CCArray = []; + $this->BCCArray = []; + $this->headers = []; + $this->debugMessage = []; + $this->debugMessageRaw = []; $this->setHeader('Date', $this->setDate()); @@ -1658,7 +1666,12 @@ protected function spoolEmail() } if (! $success) { - $this->setErrorMessage(lang('Email.sendFailure' . ($protocol === 'mail' ? 'PHPMail' : ucfirst($protocol)))); + $message = lang('Email.sendFailure' . ($protocol === 'mail' ? 'PHPMail' : ucfirst($protocol))); + + log_message('error', 'Email: ' . $message); + log_message('error', $this->printDebuggerRaw()); + + $this->setErrorMessage($message); return false; } @@ -1937,7 +1950,8 @@ protected function sendCommand($cmd, $data = '') $reply = $this->getSMTPData(); - $this->debugMessage[] = '
' . $cmd . ': ' . $reply . '
'; + $this->debugMessage[] = '
' . $cmd . ': ' . $reply . '
'; + $this->debugMessageRaw[] = $cmd . ': ' . $reply; if ($resp === null || ((int) static::substr($reply, 0, 3) !== $resp)) { $this->setErrorMessage(lang('Email.SMTPError', [$reply])); @@ -2090,8 +2104,8 @@ protected function getHostname() } /** - * @param array $include List of raw data chunks to include in the output - * Valid options are: 'headers', 'subject', 'body' + * @param array|string $include List of raw data chunks to include in the output + * Valid options are: 'headers', 'subject', 'body' * * @return string */ @@ -2119,12 +2133,21 @@ public function printDebugger($include = ['headers', 'subject', 'body']) return $msg . ($rawData === '' ? '' : '
' . $rawData . '
'); } + /** + * Returns raw debug messages + */ + private function printDebuggerRaw(): string + { + return implode("\n", $this->debugMessageRaw); + } + /** * @param string $msg */ protected function setErrorMessage($msg) { - $this->debugMessage[] = $msg . '
'; + $this->debugMessage[] = $msg . '
'; + $this->debugMessageRaw[] = $msg; } /** diff --git a/system/Filters/CSRF.php b/system/Filters/CSRF.php index 9cce85fb..6bc83405 100644 --- a/system/Filters/CSRF.php +++ b/system/Filters/CSRF.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Filters; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; @@ -44,7 +45,7 @@ class CSRF implements FilterInterface */ public function before(RequestInterface $request, $arguments = null) { - if ($request->isCLI()) { + if (! $request instanceof IncomingRequest) { return; } diff --git a/system/Filters/Honeypot.php b/system/Filters/Honeypot.php index 539513e7..419c2b47 100644 --- a/system/Filters/Honeypot.php +++ b/system/Filters/Honeypot.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Filters; use CodeIgniter\Honeypot\Exceptions\HoneypotException; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Config\Services; @@ -31,6 +32,10 @@ class Honeypot implements FilterInterface */ public function before(RequestInterface $request, $arguments = null) { + if (! $request instanceof IncomingRequest) { + return; + } + if (Services::honeypot()->hasContent($request)) { throw HoneypotException::isBot(); } diff --git a/system/Filters/InvalidChars.php b/system/Filters/InvalidChars.php index 4b1d8f7f..42aa15b1 100644 --- a/system/Filters/InvalidChars.php +++ b/system/Filters/InvalidChars.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Filters; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Security\Exceptions\SecurityException; @@ -48,7 +49,7 @@ class InvalidChars implements FilterInterface */ public function before(RequestInterface $request, $arguments = null) { - if ($request->isCLI()) { + if (! $request instanceof IncomingRequest) { return; } diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index 5f2c70b1..e81ae9ca 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\HTTP; use Config\App; +use Locale; use RuntimeException; /** @@ -214,4 +215,79 @@ public function isCLI(): bool { return true; } + + /** + * Fetch an item from GET data. + * + * @param array|string|null $index Index for item to fetch from $_GET. + * @param int|null $filter A filter name to apply. + * @param mixed|null $flags + * + * @return null + */ + public function getGet($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * Fetch an item from POST. + * + * @param array|string|null $index Index for item to fetch from $_POST. + * @param int|null $filter A filter name to apply + * @param mixed $flags + * + * @return null + */ + public function getPost($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * Fetch an item from POST data with fallback to GET. + * + * @param array|string|null $index Index for item to fetch from $_POST or $_GET + * @param int|null $filter A filter name to apply + * @param mixed $flags + * + * @return null + */ + public function getPostGet($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * Fetch an item from GET data with fallback to POST. + * + * @param array|string|null $index Index for item to be fetched from $_GET or $_POST + * @param int|null $filter A filter name to apply + * @param mixed $flags + * + * @return null + */ + public function getGetPost($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * @param array|string|null $index + * + * @return array|null + */ + private function returnNullOrEmptyArray($index) + { + return ($index === null || is_array($index)) ? [] : null; + } + + /** + * Gets the current locale, with a fallback to the default + * locale if none is set. + */ + public function getLocale(): string + { + return Locale::getDefault(); + } } diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php index dfb9dced..d0e82257 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -27,7 +27,7 @@ class Negotiate /** * Request * - * @var IncomingRequest|RequestInterface + * @var IncomingRequest */ protected $request; @@ -37,6 +37,8 @@ class Negotiate public function __construct(?RequestInterface $request = null) { if ($request !== null) { + assert($request instanceof IncomingRequest); + $this->request = $request; } } @@ -48,6 +50,8 @@ public function __construct(?RequestInterface $request = null) */ public function setRequest(RequestInterface $request) { + assert($request instanceof IncomingRequest); + $this->request = $request; return $this; diff --git a/system/HTTP/RequestInterface.php b/system/HTTP/RequestInterface.php index e7bc14a3..ca3229db 100644 --- a/system/HTTP/RequestInterface.php +++ b/system/HTTP/RequestInterface.php @@ -13,10 +13,6 @@ /** * Expected behavior of an HTTP request - * - * @mixin IncomingRequest - * @mixin CLIRequest - * @mixin CURLRequest */ interface RequestInterface { diff --git a/system/Honeypot/Honeypot.php b/system/Honeypot/Honeypot.php index 178f5662..fdd8fe95 100644 --- a/system/Honeypot/Honeypot.php +++ b/system/Honeypot/Honeypot.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Honeypot; use CodeIgniter\Honeypot\Exceptions\HoneypotException; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Config\Honeypot as HoneypotConfig; @@ -59,6 +60,8 @@ public function __construct(HoneypotConfig $config) */ public function hasContent(RequestInterface $request) { + assert($request instanceof IncomingRequest); + return ! empty($request->getPost($this->config->name)); } diff --git a/system/Log/Handlers/BaseHandler.php b/system/Log/Handlers/BaseHandler.php index 97d1281d..c19ecb0b 100644 --- a/system/Log/Handlers/BaseHandler.php +++ b/system/Log/Handlers/BaseHandler.php @@ -47,17 +47,6 @@ public function canHandle(string $level): bool return in_array($level, $this->handles, true); } - /** - * Handles logging the message. - * If the handler returns false, then execution of handlers - * will stop. Any handlers that have not run, yet, will not - * be run. - * - * @param string $level - * @param string $message - */ - abstract public function handle($level, $message): bool; - /** * Stores the date format to use while logging messages. */ diff --git a/system/Model.php b/system/Model.php index 04bd6b15..5353d9a5 100644 --- a/system/Model.php +++ b/system/Model.php @@ -40,6 +40,7 @@ * * @property BaseConnection $db * + * @method $this groupBy($by, ?bool $escape = null) * @method $this havingIn(?string $key = null, $values = null, ?bool $escape = null) * @method $this havingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false) * @method $this havingNotIn(?string $key = null, $values = null, ?bool $escape = null) diff --git a/system/Security/Security.php b/system/Security/Security.php index 36f4a730..df5bb17e 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -13,6 +13,7 @@ use CodeIgniter\Cookie\Cookie; use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\Response; use CodeIgniter\Security\Exceptions\SecurityException; @@ -321,6 +322,8 @@ public function verify(RequestInterface $request) */ private function removeTokenInRequest(RequestInterface $request): void { + assert($request instanceof Request); + $json = json_decode($request->getBody() ?? ''); if (isset($_POST[$this->tokenName])) { @@ -336,6 +339,8 @@ private function removeTokenInRequest(RequestInterface $request): void private function getPostedToken(RequestInterface $request): ?string { + assert($request instanceof IncomingRequest); + // Does the token exist in POST, HEADER or optionally php:://input - json data. if ($request->hasHeader($this->headerName) && ! empty($request->header($this->headerName)->getValue())) { $tokenName = $request->header($this->headerName)->getValue(); @@ -580,6 +585,8 @@ private function saveHashInCookie(): void */ protected function sendCookie(RequestInterface $request) { + assert($request instanceof IncomingRequest); + if ($this->cookie->isSecure() && ! $request->isSecure()) { return false; } diff --git a/system/Session/Session.php b/system/Session/Session.php index f38de5a3..f864a09b 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -25,6 +25,8 @@ * * Session configuration is done through session variables and cookie related * variables in app/config/App.php + * + * @property string $session_id */ class Session implements SessionInterface { diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index 2d95b496..16693f26 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -139,7 +139,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @return mixed + * @return bool|object */ protected function execute(string $sql) { @@ -179,8 +179,10 @@ public function insertID(): int /** * Generates the SQL for listing tables in a platform-dependent manner. + * + * @param string|null $tableName If $tableName is provided will return only this table if exists. */ - protected function _listTables(bool $constrainByPrefix = false): string + protected function _listTables(bool $constrainByPrefix = false, ?string $tableName = null): string { return ''; } diff --git a/system/ThirdParty/Kint/Kint.php b/system/ThirdParty/Kint/Kint.php index e2145b06..701283c2 100644 --- a/system/ThirdParty/Kint/Kint.php +++ b/system/ThirdParty/Kint/Kint.php @@ -150,6 +150,7 @@ class Kint 'Kint\\Parser\\ClosurePlugin', 'Kint\\Parser\\ColorPlugin', 'Kint\\Parser\\DateTimePlugin', + 'Kint\\Parser\\EnumPlugin', 'Kint\\Parser\\FsPathPlugin', 'Kint\\Parser\\IteratorPlugin', 'Kint\\Parser\\JsonPlugin', diff --git a/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php b/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php index 89601af2..7d2673f8 100644 --- a/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php +++ b/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php @@ -30,6 +30,7 @@ use Kint\Zval\Value; use ReflectionClass; use ReflectionProperty; +use UnitEnum; class ClassStaticsPlugin extends Plugin { @@ -56,6 +57,11 @@ public function parse(&$var, Value &$o, $trigger) $consts = []; foreach ($reflection->getConstants() as $name => $val) { + // Skip enum constants + if ($var instanceof UnitEnum && $val instanceof UnitEnum && $o->classname == \get_class($val)) { + continue; + } + $const = Value::blank($name, '\\'.$class.'::'.$name); $const->const = true; $const->depth = $o->depth + 1; diff --git a/system/ThirdParty/Kint/Parser/EnumPlugin.php b/system/ThirdParty/Kint/Parser/EnumPlugin.php new file mode 100644 index 00000000..3fe25f4e --- /dev/null +++ b/system/ThirdParty/Kint/Parser/EnumPlugin.php @@ -0,0 +1,86 @@ +contents = []; + + foreach ($var->cases() as $case) { + $base_obj = Value::blank($class.'::'.$case->name, '\\'.$class.'::'.$case->name); + $base_obj->depth = $o->depth + 1; + + if ($var instanceof BackedEnum) { + $c = $case->value; + $cases->contents[] = $this->parser->parse($c, $base_obj); + } else { + $cases->contents[] = $base_obj; + } + } + + self::$cache[$class] = $cases; + } + + $object = new EnumValue($var); + $object->transplant($o); + + $object->addRepresentation(self::$cache[$class], 0); + + $o = $object; + } +} diff --git a/system/ThirdParty/Kint/Renderer/Text/EnumPlugin.php b/system/ThirdParty/Kint/Renderer/Text/EnumPlugin.php new file mode 100644 index 00000000..efb5ba5d --- /dev/null +++ b/system/ThirdParty/Kint/Renderer/Text/EnumPlugin.php @@ -0,0 +1,44 @@ +depth) { + $out .= $this->renderer->colorTitle($this->renderer->renderTitle($o)).PHP_EOL; + } + + $out .= $this->renderer->renderHeader($o).PHP_EOL; + + return $out; + } +} diff --git a/system/ThirdParty/Kint/Renderer/TextRenderer.php b/system/ThirdParty/Kint/Renderer/TextRenderer.php index 0cfba527..3421e132 100644 --- a/system/ThirdParty/Kint/Renderer/TextRenderer.php +++ b/system/ThirdParty/Kint/Renderer/TextRenderer.php @@ -42,6 +42,7 @@ class TextRenderer extends Renderer 'microtime' => 'Kint\\Renderer\\Text\\MicrotimePlugin', 'recursion' => 'Kint\\Renderer\\Text\\RecursionPlugin', 'trace' => 'Kint\\Renderer\\Text\\TracePlugin', + 'enum' => 'Kint\\Renderer\\Text\\EnumPlugin', ]; /** @@ -55,6 +56,7 @@ class TextRenderer extends Renderer 'Kint\\Parser\\MicrotimePlugin', 'Kint\\Parser\\StreamPlugin', 'Kint\\Parser\\TracePlugin', + 'Kint\\Parser\\EnumPlugin', ]; /** diff --git a/system/ThirdParty/Kint/Zval/EnumValue.php b/system/ThirdParty/Kint/Zval/EnumValue.php new file mode 100644 index 00000000..018eb68f --- /dev/null +++ b/system/ThirdParty/Kint/Zval/EnumValue.php @@ -0,0 +1,62 @@ +enumval = $enumval; + } + + public function getValueShort() + { + if ($this->enumval instanceof BackedEnum) { + if (\is_string($this->enumval->value)) { + return '"'.$this->enumval->value.'"'; + } + if (\is_int($this->enumval->value)) { + return (string) $this->enumval->value; + } + } + } + + public function getType() + { + return $this->classname.'::'.$this->enumval->name; + } + + public function getSize() + { + } +} diff --git a/system/ThirdParty/Kint/resources/compiled/rich.js b/system/ThirdParty/Kint/resources/compiled/rich.js index f15d38ff..2f0ef6a1 100644 --- a/system/ThirdParty/Kint/resources/compiled/rich.js +++ b/system/ThirdParty/Kint/resources/compiled/rich.js @@ -1 +1 @@ -void 0===window.kintRich&&(window.kintRich=function(){"use strict";var l={selectText:function(e){var t=window.getSelection(),a=document.createRange();a.selectNodeContents(e),t.removeAllRanges(),t.addRange(a)},toggle:function(e,t){var a=l.getChildren(e);a&&(e.classList.toggle("kint-show",t),1===a.childNodes.length&&(a=a.childNodes[0].childNodes[0])&&a.classList&&a.classList.contains("kint-parent")&&l.toggle(a,t))},toggleChildren:function(e,t){var a=l.getChildren(e);if(a){var o=a.getElementsByClassName("kint-parent"),n=o.length;for(void 0===t&&(t=e.classList.contains("kint-show"));n--;)l.toggle(o[n],t)}},switchTab:function(e){var t=e.previousSibling,a=0;for(e.parentNode.getElementsByClassName("kint-active-tab")[0].classList.remove("kint-active-tab"),e.classList.add("kint-active-tab");t;)1===t.nodeType&&a++,t=t.previousSibling;for(var o=e.parentNode.nextSibling.childNodes,n=0;n"},openInNewWindow:function(e){var t=window.open();t&&(t.document.open(),t.document.write(l.mktag("html")+l.mktag("head")+l.mktag("title")+"Kint ("+(new Date).toISOString()+")"+l.mktag("/title")+l.mktag('meta charset="utf-8"')+l.mktag('script class="kint-rich-script" nonce="'+l.script.nonce+'"')+l.script.innerHTML+l.mktag("/script")+l.mktag('style class="kint-rich-style" nonce="'+l.style.nonce+'"')+l.style.innerHTML+l.mktag("/style")+l.mktag("/head")+l.mktag("body")+'
'+e.parentNode.outerHTML+"
"+l.mktag("/body")),t.document.close())},sortTable:function(e,a){var t=e.tBodies[0];[].slice.call(e.tBodies[0].rows).sort(function(e,t){if(e=e.cells[a].textContent.trim().toLocaleLowerCase(),t=t.cells[a].textContent.trim().toLocaleLowerCase(),isNaN(e)||isNaN(t)){if(isNaN(e)&&!isNaN(t))return 1;if(isNaN(t)&&!isNaN(e))return-1}else e=parseFloat(e),t=parseFloat(t);return eli:not(.kint-active-tab)").forEach(function(e){l.isFolderOpen()&&!l.folder.contains(e)||0===e.offsetWidth&&0===e.offsetHeight||l.keyboardNav.targets.push(e)}),e&&-1!==l.keyboardNav.targets.indexOf(e)&&(l.keyboardNav.target=l.keyboardNav.targets.indexOf(e))},sync:function(e){var t=document.querySelector(".kint-focused");t&&t.classList.remove("kint-focused"),l.keyboardNav.active&&((t=l.keyboardNav.targets[l.keyboardNav.target]).classList.add("kint-focused"),e||l.keyboardNav.scroll(t))},scroll:function(e){var t,a;e!==l.folder.querySelector("dt > nav")&&(a=(t=function(e){return e.offsetTop+(e.offsetParent?t(e.offsetParent):0)})(e),l.isFolderOpen()?(e=l.folder.querySelector("dd.kint-foldout")).scrollTo(0,a-e.clientHeight/2):window.scrollTo(0,a-window.innerHeight/2))},moveCursor:function(e){for(l.keyboardNav.target+=e;l.keyboardNav.target<0;)l.keyboardNav.target+=l.keyboardNav.targets.length;for(;l.keyboardNav.target>=l.keyboardNav.targets.length;)l.keyboardNav.target-=l.keyboardNav.targets.length;l.keyboardNav.sync()},setCursor:function(e){if(l.isFolderOpen()&&!l.folder.contains(e))return!1;l.keyboardNav.fetchTargets();for(var t=0;t"},openInNewWindow:function(e){var t=window.open();t&&(t.document.open(),t.document.write(l.mktag("html")+l.mktag("head")+l.mktag("title")+"Kint ("+(new Date).toISOString()+")"+l.mktag("/title")+l.mktag('meta charset="utf-8"')+l.mktag('script class="kint-rich-script" nonce="'+l.script.nonce+'"')+l.script.innerHTML+l.mktag("/script")+l.mktag('style class="kint-rich-style" nonce="'+l.style.nonce+'"')+l.style.innerHTML+l.mktag("/style")+l.mktag("/head")+l.mktag("body")+'
'+e.parentNode.outerHTML+"
"+l.mktag("/body")),t.document.close())},sortTable:function(e,a){var t=e.tBodies[0];[].slice.call(e.tBodies[0].rows).sort(function(e,t){if(e=e.cells[a].textContent.trim().toLocaleLowerCase(),t=t.cells[a].textContent.trim().toLocaleLowerCase(),isNaN(e)||isNaN(t)){if(isNaN(e)&&!isNaN(t))return 1;if(isNaN(t)&&!isNaN(e))return-1}else e=parseFloat(e),t=parseFloat(t);return eli:not(.kint-active-tab)").forEach(function(e){l.isFolderOpen()&&!l.folder.contains(e)||0===e.offsetWidth&&0===e.offsetHeight||l.keyboardNav.targets.push(e)}),e&&-1!==l.keyboardNav.targets.indexOf(e)&&(l.keyboardNav.target=l.keyboardNav.targets.indexOf(e))},sync:function(e){var t=document.querySelector(".kint-focused");t&&t.classList.remove("kint-focused"),l.keyboardNav.active&&((t=l.keyboardNav.targets[l.keyboardNav.target]).classList.add("kint-focused"),e||l.keyboardNav.scroll(t))},scroll:function(e){var t,a;e!==l.folder.querySelector("dt > nav")&&(a=(t=function(e){return e.offsetTop+(e.offsetParent?t(e.offsetParent):0)})(e),l.isFolderOpen()?(e=l.folder.querySelector("dd.kint-foldout")).scrollTo(0,a-e.clientHeight/2):window.scrollTo(0,a-window.innerHeight/2))},moveCursor:function(e){for(l.keyboardNav.target+=e;l.keyboardNav.target<0;)l.keyboardNav.target+=l.keyboardNav.targets.length;for(;l.keyboardNav.target>=l.keyboardNav.targets.length;)l.keyboardNav.target-=l.keyboardNav.targets.length;l.keyboardNav.sync()},setCursor:function(e){if(l.isFolderOpen()&&!l.folder.contains(e))return!1;l.keyboardNav.fetchTargets();for(var t=0;trequest = $request; } diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 7e4a55db..ed370a2f 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -168,7 +168,7 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup if (strpos($field, '*') !== false) { // Process multiple fields foreach ($values as $dotField => $value) { - $this->processRules($dotField, $setup['label'] ?? $field, $value, $rules, $data); + $this->processRules($dotField, $setup['label'] ?? $field, $value, $rules, $data, $field); } } else { // Process single field @@ -201,10 +201,17 @@ public function check($value, string $rule, array $errors = []): bool * * @param array|string $value * @param array|null $rules - * @param array $data + * @param array $data The array of data to validate, with `DBGroup`. + * @param string|null $originalField The original asterisk field name like "foo.*.bar". */ - protected function processRules(string $field, ?string $label, $value, $rules = null, ?array $data = null): bool - { + protected function processRules( + string $field, + ?string $label, + $value, + $rules = null, + ?array $data = null, + ?string $originalField = null + ): bool { if ($data === null) { throw new InvalidArgumentException('You must supply the parameter: data.'); } @@ -333,7 +340,8 @@ protected function processRules(string $field, ?string $label, $value, $rules = $field, $label, $param, - (string) $value + (string) $value, + $originalField ); return false; @@ -706,13 +714,21 @@ public function setError(string $field, string $error): ValidationInterface * * @param string|null $value The value that caused the validation to fail. */ - protected function getErrorMessage(string $rule, string $field, ?string $label = null, ?string $param = null, ?string $value = null): string - { + protected function getErrorMessage( + string $rule, + string $field, + ?string $label = null, + ?string $param = null, + ?string $value = null, + ?string $originalField = null + ): string { $param ??= ''; // Check if custom message has been defined by user if (isset($this->customErrors[$field][$rule])) { $message = lang($this->customErrors[$field][$rule]); + } elseif (null !== $originalField && isset($this->customErrors[$originalField][$rule])) { + $message = lang($this->customErrors[$originalField][$rule]); } else { // Try to grab a localized version of the message... // lang() will return the rule name back if not found,