diff --git a/system/CLI/Commands.php b/system/CLI/Commands.php index 30bd2c26..21157141 100644 --- a/system/CLI/Commands.php +++ b/system/CLI/Commands.php @@ -125,7 +125,7 @@ public function discoverCommands() /** @var BaseCommand $class */ $class = new $className($this->logger, $this); - if (isset($class->group)) { + if (isset($class->group) && ! isset($this->commands[$class->name])) { $this->commands[$class->name] = [ 'class' => $className, 'file' => $file, diff --git a/system/CLI/GeneratorTrait.php b/system/CLI/GeneratorTrait.php index 258f1237..065197be 100644 --- a/system/CLI/GeneratorTrait.php +++ b/system/CLI/GeneratorTrait.php @@ -516,7 +516,7 @@ protected function setEnabledSuffixing(bool $enabledSuffixing) * Gets a single command-line option. Returns TRUE if the option exists, * but doesn't have a value, and is simply acting as a flag. */ - protected function getOption(string $name): string|bool|null + protected function getOption(string $name): bool|string|null { if (! array_key_exists($name, $this->params)) { return CLI::getOption($name); diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php index 9e1003d1..22f4e435 100644 --- a/system/Cache/Handlers/RedisHandler.php +++ b/system/Cache/Handlers/RedisHandler.php @@ -175,18 +175,17 @@ public function delete(string $key) */ public function deleteMatching(string $pattern) { + /** @var list $matchedKeys */ $matchedKeys = []; + $pattern = static::validateKey($pattern, $this->prefix); $iterator = null; do { - // Scan for some keys + /** @var false|list|Redis $keys */ $keys = $this->redis->scan($iterator, $pattern); - // Redis may return empty results, so protect against that - if ($keys !== false) { - foreach ($keys as $key) { - $matchedKeys[] = $key; - } + if (is_array($keys)) { + $matchedKeys = [...$matchedKeys, ...$keys]; } } while ($iterator > 0); diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 65f5dd22..95feae81 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -56,7 +56,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.5.2'; + public const CI_VERSION = '4.5.3'; /** * App startup time. @@ -353,7 +353,7 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon } else { try { $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse); - } catch (ResponsableInterface|DeprecatedRedirectException $e) { + } catch (DeprecatedRedirectException|ResponsableInterface $e) { $this->outputBufferingEnd(); if ($e instanceof DeprecatedRedirectException) { $e = new RedirectException($e->getMessage(), $e->getCode(), $e); diff --git a/system/Commands/Database/MigrateRollback.php b/system/Commands/Database/MigrateRollback.php index f206d7fc..e1c788e7 100644 --- a/system/Commands/Database/MigrateRollback.php +++ b/system/Commands/Database/MigrateRollback.php @@ -15,6 +15,7 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; +use CodeIgniter\Database\MigrationRunner; use Throwable; /** @@ -78,10 +79,23 @@ public function run(array $params) // @codeCoverageIgnoreEnd } + /** @var MigrationRunner $runner */ $runner = service('migrations'); try { $batch = $params['b'] ?? CLI::getOption('b') ?? $runner->getLastBatch() - 1; + + if (is_string($batch)) { + if (! ctype_digit($batch)) { + CLI::error('Invalid batch number: ' . $batch, 'light_gray', 'red'); + CLI::newLine(); + + return EXIT_ERROR; + } + + $batch = (int) $batch; + } + CLI::write(lang('Migrations.rollingBack') . ' ' . $batch, 'yellow'); if (! $runner->regress($batch)) { diff --git a/system/Commands/Database/ShowTableInfo.php b/system/Commands/Database/ShowTableInfo.php index 05dcfe4d..147b6eb6 100644 --- a/system/Commands/Database/ShowTableInfo.php +++ b/system/Commands/Database/ShowTableInfo.php @@ -188,6 +188,11 @@ private function restoreDBPrefix(): void $this->db->setPrefix($this->DBPrefix); } + /** + * Show Data of Table + * + * @return void + */ private function showDataOfTable(string $tableName, int $limitRows, int $limitFieldValue) { CLI::write("Data of Table \"{$tableName}\":", 'black', 'yellow'); @@ -207,6 +212,13 @@ private function showDataOfTable(string $tableName, int $limitRows, int $limitFi CLI::table($this->tbody, $thead); } + /** + * Show All Tables + * + * @param list $tables + * + * @return void + */ private function showAllTables(array $tables) { CLI::write('The following is a list of the names of all database tables:', 'black', 'yellow'); @@ -219,6 +231,13 @@ private function showAllTables(array $tables) CLI::newLine(); } + /** + * Make body for table + * + * @param list $tables + * + * @return list> + */ private function makeTbodyForShowAllTables(array $tables): array { $this->removeDBPrefix(); @@ -244,6 +263,11 @@ private function makeTbodyForShowAllTables(array $tables): array return $this->tbody; } + /** + * Make table rows + * + * @return list> + */ private function makeTableRows( string $tableName, int $limitRows, diff --git a/system/Commands/ListCommands.php b/system/Commands/ListCommands.php index 778f8f95..87c3e96b 100644 --- a/system/Commands/ListCommands.php +++ b/system/Commands/ListCommands.php @@ -85,6 +85,8 @@ public function run(array $params) /** * Lists the commands with accompanying info. + * + * @return void */ protected function listFull(array $commands) { @@ -126,6 +128,8 @@ protected function listFull(array $commands) /** * Lists the commands only. + * + * @return void */ protected function listSimple(array $commands) { diff --git a/system/Commands/Server/Serve.php b/system/Commands/Server/Serve.php index c32cd656..82e58998 100644 --- a/system/Commands/Server/Serve.php +++ b/system/Commands/Server/Serve.php @@ -103,7 +103,7 @@ public function run(array $params) $docroot = escapeshellarg(FCPATH); // Mimic Apache's mod_rewrite functionality with user settings. - $rewrite = escapeshellarg(__DIR__ . '/rewrite.php'); + $rewrite = escapeshellarg(SYSTEMPATH . 'rewrite.php'); // Call PHP's built-in webserver, making sure to set our // base path to the public folder, and to use the rewrite file diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php index 8cbd81fb..0d426a3b 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php @@ -27,6 +27,7 @@ final class AutoRouteCollector * @param string $namespace namespace to search * @param list $protectedControllers List of controllers in Defined * Routes that should not be accessed via Auto-Routing. + * @param list $httpMethods * @param string $prefix URI prefix for Module Routing */ public function __construct( @@ -91,6 +92,13 @@ public function get(): array return $tbody; } + /** + * Adding Filters + * + * @param list> $routes + * + * @return list> + */ private function addFilters($routes) { $filterCollector = new FilterCollector(true); diff --git a/system/Commands/Utilities/Routes/FilterFinder.php b/system/Commands/Utilities/Routes/FilterFinder.php index 82ea2697..7971e5c1 100644 --- a/system/Commands/Utilities/Routes/FilterFinder.php +++ b/system/Commands/Utilities/Routes/FilterFinder.php @@ -15,6 +15,7 @@ use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Filters\Filters; +use CodeIgniter\HTTP\Exceptions\BadRequestException; use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\Router\Router; use Config\Feature; @@ -72,7 +73,7 @@ public function find(string $uri): array 'before' => [], 'after' => [], ]; - } catch (PageNotFoundException) { + } catch (BadRequestException|PageNotFoundException) { return [ 'before' => [''], 'after' => [''], diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index cd770dc6..5bd55403 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -346,6 +346,8 @@ public static function serviceExists(string $name): ?string * Reset shared instances and mocks for testing. * * @return void + * + * @testTag only available to test code */ public static function reset(bool $initAutoloader = true) { @@ -362,6 +364,8 @@ public static function reset(bool $initAutoloader = true) * Resets any mock and shared instances for a single service. * * @return void + * + * @testTag only available to test code */ public static function resetSingle(string $name) { @@ -375,6 +379,8 @@ public static function resetSingle(string $name) * @param object $mock * * @return void + * + * @testTag only available to test code */ public static function injectMock(string $name, $mock) { diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index f9ca1efd..4e6e422b 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -1448,11 +1448,12 @@ public function orHaving($key, $value = null, ?bool $escape = null) */ public function orderBy(string $orderBy, string $direction = '', ?bool $escape = null) { - $qbOrderBy = []; if ($orderBy === '') { return $this; } + $qbOrderBy = []; + $direction = strtoupper(trim($direction)); if ($direction === 'RANDOM') { @@ -1463,7 +1464,7 @@ public function orderBy(string $orderBy, string $direction = '', ?bool $escape = $direction = in_array($direction, ['ASC', 'DESC'], true) ? ' ' . $direction : ''; } - if (! is_bool($escape)) { + if ($escape === null) { $escape = $this->db->protectIdentifiers; } @@ -1474,8 +1475,6 @@ public function orderBy(string $orderBy, string $direction = '', ?bool $escape = 'escape' => false, ]; } else { - $qbOrderBy = []; - foreach (explode(',', $orderBy) as $field) { $qbOrderBy[] = ($direction === '' && preg_match('/\s+(ASC|DESC)$/i', rtrim($field), $match, PREG_OFFSET_CAPTURE)) ? [ diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 57c37d8a..6581dc7c 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -477,6 +477,8 @@ public function initialize() /** * Close the database connection. + * + * @return void */ public function close() { @@ -736,6 +738,8 @@ public function simpleQuery(string $sql) * Disable Transactions * * This permits transactions to be disabled at run-time. + * + * @return void */ public function transOff() { @@ -1454,7 +1458,7 @@ protected function getDriverFunctionPrefix(): string /** * Returns an array of table names * - * @return array|false + * @return false|list * * @throws DatabaseException */ @@ -1481,6 +1485,7 @@ public function listTables(bool $constrainByPrefix = false) $query = $this->query($sql); foreach ($query->getResultArray() as $row) { + /** @var string $table */ $table = $row['table_name'] ?? $row['TABLE_NAME'] ?? $row[array_key_first($row)]; $this->dataCache['table_names'][] = $table; @@ -1531,7 +1536,7 @@ public function tableExists(string $tableName, bool $cached = true): bool /** * Fetch Field Names * - * @return array|false + * @return false|list * * @throws DatabaseException */ @@ -1608,7 +1613,7 @@ public function getIndexData(string $table) /** * Returns an object with foreign key data * - * @return array + * @return array */ public function getForeignKeyData(string $table) { @@ -1769,7 +1774,9 @@ abstract protected function _listColumns(string $table = ''); /** * Platform-specific field data information. * - * @see getFieldData() + * @see getFieldData() + * + * @return list */ abstract protected function _fieldData(string $table): array; diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index b25f2e1a..068dfba5 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -237,6 +237,8 @@ public function connect(bool $persistent = false) /** * Keep or establish the connection if no queries have been sent for * a length of time exceeding the server's idle timeout. + * + * @return void */ public function reconnect() { diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index be7eb37c..f7983a69 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -104,6 +104,10 @@ class Connection extends BaseConnection */ private function isValidDSN(): bool { + if ($this->DSN === null || $this->DSN === '') { + return false; + } + foreach ($this->validDSNs as $regexp) { if (preg_match($regexp, $this->DSN)) { return true; @@ -120,13 +124,13 @@ private function isValidDSN(): bool */ public function connect(bool $persistent = false) { - if (empty($this->DSN) && ! $this->isValidDSN()) { + if (! $this->isValidDSN()) { $this->buildDSN(); } $func = $persistent ? 'oci_pconnect' : 'oci_connect'; - return empty($this->charset) + return ($this->charset === '') ? $func($this->username, $this->password, $this->DSN) : $func($this->username, $this->password, $this->DSN, $this->charset); } @@ -632,7 +636,7 @@ protected function buildDSN() } $isEasyConnectableHostName = $this->hostname !== '' && ! str_contains($this->hostname, '/') && ! str_contains($this->hostname, ':'); - $easyConnectablePort = ! empty($this->port) && ctype_digit($this->port) ? ':' . $this->port : ''; + $easyConnectablePort = ($this->port !== '') && ctype_digit((string) $this->port) ? ':' . $this->port : ''; $easyConnectableDatabase = $this->database !== '' ? '/' . ltrim($this->database, '/') : ''; if ($isEasyConnectableHostName && ($easyConnectablePort !== '' || $easyConnectableDatabase !== '')) { diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index c22dc860..d3af2380 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -104,6 +104,8 @@ public function connect(bool $persistent = false) /** * Converts the DSN with semicolon syntax. + * + * @return void */ private function convertDSN() { @@ -143,6 +145,8 @@ private function convertDSN() /** * Keep or establish the connection if no queries have been sent for * a length of time exceeding the server's idle timeout. + * + * @return void */ public function reconnect() { diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 0626a63c..7e008e27 100644 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -164,6 +164,8 @@ public function getAllErrorMessages(): string /** * Keep or establish the connection if no queries have been sent for * a length of time exceeding the server's idle timeout. + * + * @return void */ public function reconnect() { diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index 94543418..9945d41d 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -55,6 +55,9 @@ class Connection extends BaseConnection */ protected $busyTimeout; + /** + * @return void + */ public function initialize() { parent::initialize(); @@ -101,6 +104,8 @@ public function connect(bool $persistent = false) /** * Keep or establish the connection if no queries have been sent for * a length of time exceeding the server's idle timeout. + * + * @return void */ public function reconnect() { @@ -211,7 +216,7 @@ protected function _listColumns(string $table = ''): string } /** - * @return array|false + * @return false|list * * @throws DatabaseException */ diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 0db5ebc1..63cdd770 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -162,7 +162,7 @@ private function searchFirstController(): bool $segment = array_shift($segments); $controllerPos++; - $class = $this->translateURIDashes($segment); + $class = $this->translateURI($segment); // as soon as we encounter any segment that is not PSR-4 compliant, stop searching if (! $this->isValidSegment($class)) { @@ -209,7 +209,7 @@ private function searchLastDefaultController(): bool } $namespaces = array_map( - fn ($segment) => $this->translateURIDashes($segment), + fn ($segment) => $this->translateURI($segment), $segments ); @@ -307,7 +307,7 @@ public function getRoute(string $uri, string $httpVerb): array $method = ''; if ($methodParam !== null) { - $method = $httpVerb . $this->translateURIDashes($methodParam); + $method = $httpVerb . $this->translateURI($methodParam); $this->checkUriForMethod($method); } @@ -519,7 +519,17 @@ private function checkUriForMethod(string $method): void return; } - if (! in_array($method, get_class_methods($this->controller), true)) { + if ( + // For example, if `getSomeMethod()` exists in the controller, only + // the URI `controller/some-method` should be accessible. But if a + // visitor navigates to the URI `controller/somemethod`, `getSomemethod()` + // will be checked, and `method_exists()` will return true because + // method names in PHP are case-insensitive. + method_exists($this->controller, $method) + // But we do not permit `controller/somemethod`, so check the exact + // method name. + && ! in_array($method, get_class_methods($this->controller), true) + ) { throw new PageNotFoundException( '"' . $this->controller . '::' . $method . '()" is not found.' ); @@ -536,7 +546,10 @@ private function isValidSegment(string $segment): bool return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment); } - private function translateURIDashes(string $segment): string + /** + * Translates URI segment to CamelCase or replaces `-` with `_`. + */ + private function translateURI(string $segment): string { if ($this->translateUriToCamelCase) { if (strtolower($segment) !== $segment) { diff --git a/system/Router/DefinedRouteCollector.php b/system/Router/DefinedRouteCollector.php index 8d4f1dc5..2452a141 100644 --- a/system/Router/DefinedRouteCollector.php +++ b/system/Router/DefinedRouteCollector.php @@ -38,6 +38,10 @@ public function collect(): Generator $routes = $this->routeCollection->getRoutes($method); foreach ($routes as $route => $handler) { + // The route key should be a string, but it is stored as an array key, + // it might be an integer. + $route = (string) $route; + if (is_string($handler) || $handler instanceof Closure) { if ($handler instanceof Closure) { $view = $this->routeCollection->getRoutesOptions($route, $method)['view'] ?? false; diff --git a/system/Router/Router.php b/system/Router/Router.php index de996f53..dc778cd0 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -187,6 +187,7 @@ public function __construct(RouteCollectionInterface $routes, ?Request $request * * @return (Closure(mixed...): (ResponseInterface|string|void))|string Controller classname or Closure * + * @throws BadRequestException * @throws PageNotFoundException * @throws RedirectException */ diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index 020d6e3d..ed814240 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -38,6 +38,11 @@ class MockConnection extends BaseConnection public $database; public $lastQuery; + /** + * @param mixed $return + * + * @return $this + */ public function shouldReturn(string $method, $return) { $this->returnValues[$method] = $return; @@ -127,13 +132,13 @@ public function reconnect(): bool /** * Select a specific database table to use. * - * @return mixed + * @return bool */ public function setDatabase(string $databaseName) { $this->database = $databaseName; - return $this; + return true; } /** diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index eb87ef3f..bfc661c2 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -399,8 +399,10 @@ private function processIfExist(string $field, array $rules, array $data) break; } } - } else { + } elseif (str_contains($field, '.')) { $dataIsExisting = array_key_exists($ifExistField, $flattenedData); + } else { + $dataIsExisting = array_key_exists($ifExistField, $data); } if (! $dataIsExisting) { diff --git a/system/Commands/Server/rewrite.php b/system/rewrite.php similarity index 93% rename from system/Commands/Server/rewrite.php rename to system/rewrite.php index f936cd63..aba45604 100644 --- a/system/Commands/Server/rewrite.php +++ b/system/rewrite.php @@ -21,11 +21,6 @@ */ // @codeCoverageIgnoreStart -// Avoid this file run when listing commands -if (PHP_SAPI === 'cli') { - return; -} - $uri = urldecode( parse_url('https://codeigniter.com' . $_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '' );