From e3921233a20daf05ae9d2b50ca41ce97748e1d6d Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 17 Jun 2023 23:13:38 +0000 Subject: [PATCH] Release v4.3.6 --- system/Autoloader/FileLocator.php | 9 +- system/CodeIgniter.php | 4 +- system/Common.php | 6 +- system/Config/BaseConfig.php | 2 +- system/Config/Factories.php | 18 +- system/Database/BaseBuilder.php | 2 +- system/Database/Config.php | 2 +- system/Database/Database.php | 22 ++- system/Database/Postgre/Connection.php | 45 ++++- system/Encryption/Encryption.php | 2 +- system/Encryption/Handlers/BaseHandler.php | 2 +- system/Entity/Entity.php | 6 +- system/Exceptions/FrameworkException.php | 2 +- system/HTTP/IncomingRequest.php | 2 +- system/Images/Handlers/GDHandler.php | 2 +- system/Model.php | 6 +- system/Router/AutoRouter.php | 2 +- system/Router/AutoRouterImproved.php | 26 +-- system/Router/AutoRouterInterface.php | 2 +- system/Router/RouteCollection.php | 6 +- system/Router/Router.php | 4 +- system/Test/FeatureTestCase.php | 3 - system/Test/FeatureTestTrait.php | 6 +- system/Validation/Rules.php | 32 ++-- system/Validation/StrictRules/Rules.php | 11 +- system/Validation/Validation.php | 191 +++++++++++++-------- system/Validation/ValidationInterface.php | 6 +- system/View/Cell.php | 6 +- system/View/View.php | 41 +++-- 29 files changed, 280 insertions(+), 188 deletions(-) diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index f58d5c79..a4f7c676 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -33,8 +33,13 @@ public function __construct(Autoloader $autoloader) * Attempts to locate a file by examining the name for a namespace * and looking through the PSR-4 namespaced files that we know about. * - * @param string $file The namespaced file to locate - * @param string|null $folder The folder within the namespace that we should look for the file. + * @param string $file The relative file path or namespaced file to + * locate. If not namespaced, search in the app + * folder. + * @param string|null $folder The folder within the namespace that we should + * look for the file. If $file does not contain + * this value, it will be appended to the namespace + * folder. * @param string $ext The file extension the file should have. * * @return false|string The path to the file, or false if not found. diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 943fa360..829b65ea 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.3.5'; + public const CI_VERSION = '4.3.6'; /** * App startup time. @@ -310,8 +310,6 @@ private function configureKint(): void * makes all of the pieces work together. * * @return ResponseInterface|void - * - * @throws RedirectException */ public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false) { diff --git a/system/Common.php b/system/Common.php index 2a98253c..8d366973 100644 --- a/system/Common.php +++ b/system/Common.php @@ -744,11 +744,7 @@ function is_windows(?bool $mock = null): bool $mocked = $mock; } - if (isset($mocked)) { - return $mocked; - } - - return DIRECTORY_SEPARATOR === '\\'; + return $mocked ?? DIRECTORY_SEPARATOR === '\\'; } } diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 359fcc14..300e9ffa 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -86,7 +86,7 @@ public function __construct() /** * Initialization an environment-specific configuration setting * - * @param mixed $property + * @param array|bool|float|int|string|null $property * * @return void */ diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 7abd3f5c..d752f55c 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -24,6 +24,7 @@ * instantiation checks. * * @method static BaseConfig|null config(...$arguments) + * @method static Model|null models(string $name, array $options = [], ?ConnectionInterface &$conn = null) */ class Factories { @@ -69,23 +70,6 @@ class Factories */ protected static $instances = []; - /** - * This method is only to prevent PHPStan error. - * If we have a solution, we can remove this method. - * See https://github.com/codeigniter4/CodeIgniter4/pull/5358 - * - * @template T of Model - * - * @phpstan-param class-string $name - * - * @return Model - * @phpstan-return T - */ - public static function models(string $name, array $options = [], ?ConnectionInterface &$conn = null) - { - return self::__callStatic('models', [$name, $options, $conn]); - } - /** * Loads instances based on the method component name. Either * creates a new instance or returns an existing shared instance. diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index fab6e30d..07486cff 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -1997,7 +1997,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri } if (isset($this->QBOptions['setQueryAsData'])) { - $data = $this->QBOptions['setQueryAsData']; + $data = $this->QBOptions['setQueryAsData'] . "\n"; } else { $data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n"; } diff --git a/system/Database/Config.php b/system/Database/Config.php index eedbbd44..802a4da2 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -37,7 +37,7 @@ class Config extends BaseConfig protected static $factory; /** - * Creates the default + * Returns the database connection * * @param array|BaseConnection|string|null $group The name of the connection group to use, * or an array of configuration settings. diff --git a/system/Database/Database.php b/system/Database/Database.php index df3ad9ae..2818fe22 100644 --- a/system/Database/Database.php +++ b/system/Database/Database.php @@ -16,7 +16,7 @@ /** * Database Connection Factory * - * Creates and returns an instance of the appropriate DatabaseConnection + * Creates and returns an instance of the appropriate Database Connection. */ class Database { @@ -32,8 +32,7 @@ class Database protected $connections = []; /** - * Parses the connection binds and returns an instance of the driver - * ready to go. + * Parses the connection binds and creates a Database Connection instance. * * @return BaseConnection * @@ -83,7 +82,7 @@ public function loadUtils(ConnectionInterface $db): BaseUtils } /** - * Parse universal DSN string + * Parses universal DSN string * * @throws InvalidArgumentException */ @@ -121,21 +120,20 @@ protected function parseDSN(array $params): array } /** - * Initialize database driver. + * Creates a database object. * * @param string $driver Driver name. FQCN can be used. - * @param array|object $argument + * @param string $class 'Connection'|'Forge'|'Utils' + * @param array|object $argument The constructor parameter. * * @return BaseConnection|BaseUtils|Forge */ protected function initDriver(string $driver, string $class, $argument): object { - $class = $driver . '\\' . $class; + $classname = (strpos($driver, '\\') === false) + ? "CodeIgniter\\Database\\{$driver}\\{$class}" + : $driver . '\\' . $class; - if (strpos($driver, '\\') === false) { - $class = "CodeIgniter\\Database\\{$class}"; - } - - return new $class($argument); + return new $classname($argument); } } diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 8e961282..56905ec9 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -64,14 +64,11 @@ public function connect(bool $persistent = false) $this->buildDSN(); } - // Strip pgsql if exists + // Convert DSN string if (mb_strpos($this->DSN, 'pgsql:') === 0) { - $this->DSN = mb_substr($this->DSN, 6); + $this->convertDSN(); } - // Convert semicolons to spaces. - $this->DSN = str_replace(';', ' ', $this->DSN); - $this->connID = $persistent === true ? pg_pconnect($this->DSN) : pg_connect($this->DSN); if ($this->connID !== false) { @@ -92,6 +89,44 @@ public function connect(bool $persistent = false) return $this->connID; } + /** + * Converts the DSN with semicolon syntax. + */ + private function convertDSN() + { + // Strip pgsql + $this->DSN = mb_substr($this->DSN, 6); + + // Convert semicolons to spaces in DSN format like: + // pgsql:host=localhost;port=5432;dbname=database_name + // https://www.php.net/manual/en/function.pg-connect.php + $allowedParams = ['host', 'port', 'dbname', 'user', 'password', 'connect_timeout', 'options', 'sslmode', 'service']; + + $parameters = explode(';', $this->DSN); + + $output = ''; + $previousParameter = ''; + + foreach ($parameters as $parameter) { + [$key, $value] = explode('=', $parameter, 2); + if (in_array($key, $allowedParams, true)) { + if ($previousParameter !== '') { + if (array_search($key, $allowedParams, true) < array_search($previousParameter, $allowedParams, true)) { + $output .= ';'; + } else { + $output .= ' '; + } + } + $output .= $parameter; + $previousParameter = $key; + } else { + $output .= ';' . $parameter; + } + } + + $this->DSN = $output; + } + /** * Keep or establish the connection if no queries have been sent for * a length of time exceeding the server's idle timeout. diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 81b25eef..fda99062 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -149,7 +149,7 @@ public static function createKey($length = 32) * * @param string $key Property name * - * @return mixed + * @return array|string|null */ public function __get($key) { diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index 13f60e97..64195672 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -61,7 +61,7 @@ protected static function substr($str, $start, $length = null) * * @param string $key Property name * - * @return mixed + * @return array|bool|int|string|null */ public function __get($key) { diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 4247c82b..07e1e263 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -429,7 +429,7 @@ public function cast(?bool $cast = null) * * @param array|bool|float|int|object|string|null $value * - * @return $this + * @return void * * @throws Exception */ @@ -452,7 +452,7 @@ public function __set(string $key, $value = null) if (method_exists($this, $method)) { $this->{$method}($value); - return $this; + return; } // Otherwise, just the value. This allows for creation of new @@ -460,8 +460,6 @@ public function __set(string $key, $value = null) // saved. Useful for grabbing values through joins, assigning // relationships, etc. $this->attributes[$dbColumn] = $value; - - return $this; } /** diff --git a/system/Exceptions/FrameworkException.php b/system/Exceptions/FrameworkException.php index 744f4f90..333f999d 100644 --- a/system/Exceptions/FrameworkException.php +++ b/system/Exceptions/FrameworkException.php @@ -46,7 +46,7 @@ public static function forMissingExtension(string $extension) 'The framework needs the following extension(s) installed and loaded: %s.', $extension ); - // @codeCoverageIgnoreEnd + // @codeCoverageIgnoreEnd } else { $message = lang('Core.missingExtension', [$extension]); } diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 29529fd5..8eeaefdb 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -554,7 +554,7 @@ public function setLocale(string $locale) */ public function getLocale(): string { - return $this->locale ?? $this->defaultLocale; + return $this->locale; } /** diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 6063ef94..840c1bb2 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -354,7 +354,7 @@ protected function getImageResource(string $path, int $imageType) throw ImageException::forInvalidImageCreate(lang('Images.pngNotSupported')); } - return imagecreatefrompng($path); + return @imagecreatefrompng($path); case IMAGETYPE_WEBP: if (! function_exists('imagecreatefromwebp')) { diff --git a/system/Model.php b/system/Model.php index 2df1bb2f..80e4bb48 100644 --- a/system/Model.php +++ b/system/Model.php @@ -800,11 +800,7 @@ public function __get(string $name) return parent::__get($name); } - if (isset($this->builder()->{$name})) { - return $this->builder()->{$name}; - } - - return null; + return $this->builder()->{$name} ?? null; } /** diff --git a/system/Router/AutoRouter.php b/system/Router/AutoRouter.php index 3c29ff82..3096b3bb 100644 --- a/system/Router/AutoRouter.php +++ b/system/Router/AutoRouter.php @@ -80,7 +80,7 @@ public function __construct( * * @return array [directory_name, controller_name, controller_method, params] */ - public function getRoute(string $uri): array + public function getRoute(string $uri, string $httpVerb): array { $segments = explode('/', $uri); diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 39b264ec..6f2aa1af 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -58,11 +58,6 @@ final class AutoRouterImproved implements AutoRouterInterface */ private bool $translateURIDashes; - /** - * HTTP verb for the request. - */ - private string $httpVerb; - /** * The namespace for controllers. */ @@ -74,15 +69,17 @@ final class AutoRouterImproved implements AutoRouterInterface private string $defaultController; /** - * The name of the default method + * The name of the default method without HTTP verb prefix. */ private string $defaultMethod; /** * @param class-string[] $protectedControllers * @param string $defaultController Short classname + * + * @deprecated $httpVerb is deprecated. No longer used. */ - public function __construct( + public function __construct(// @phpstan-ignore-line array $protectedControllers, string $namespace, string $defaultController, @@ -93,13 +90,11 @@ public function __construct( $this->protectedControllers = $protectedControllers; $this->namespace = rtrim($namespace, '\\') . '\\'; $this->translateURIDashes = $translateURIDashes; - $this->httpVerb = $httpVerb; $this->defaultController = $defaultController; - $this->defaultMethod = $httpVerb . ucfirst($defaultMethod); + $this->defaultMethod = $defaultMethod; // Set the default values $this->controller = $this->defaultController; - $this->method = $this->defaultMethod; } /** @@ -107,8 +102,13 @@ public function __construct( * * @return array [directory_name, controller_name, controller_method, params] */ - public function getRoute(string $uri): array + public function getRoute(string $uri, string $httpVerb): array { + $httpVerb = strtolower($httpVerb); + + $defaultMethod = $httpVerb . ucfirst($this->defaultMethod); + $this->method = $defaultMethod; + $segments = explode('/', $uri); // WARNING: Directories get shifted out of the segments array. @@ -144,10 +144,10 @@ public function getRoute(string $uri): array $methodSegment = $this->translateURIDashes(array_shift($nonDirSegments)); // Prefix HTTP verb - $this->method = $this->httpVerb . ucfirst($methodSegment); + $this->method = $httpVerb . ucfirst($methodSegment); // Prevent access to default method path - if (strtolower($this->method) === strtolower($this->defaultMethod)) { + if (strtolower($this->method) === strtolower($defaultMethod)) { throw new PageNotFoundException( 'Cannot access the default method "' . $this->method . '" with the method name URI path.' ); diff --git a/system/Router/AutoRouterInterface.php b/system/Router/AutoRouterInterface.php index 9ecdd3ec..6d98aec4 100644 --- a/system/Router/AutoRouterInterface.php +++ b/system/Router/AutoRouterInterface.php @@ -21,5 +21,5 @@ interface AutoRouterInterface * * @return array [directory_name, controller_name, controller_method, params] */ - public function getRoute(string $uri): array; + public function getRoute(string $uri, string $httpVerb): array; } diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 50b73616..66724935 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -150,7 +150,7 @@ class RouteCollection implements RouteCollectionInterface /** * The current method that the script is being called by. * - * @var string + * @var string HTTP verb (lower case) like `get`,`post` or `*` */ protected $HTTPVerb = '*'; @@ -550,11 +550,13 @@ public function getHTTPVerb(): string * Sets the current HTTP verb. * Used primarily for testing. * + * @param string $verb HTTP verb + * * @return $this */ public function setHTTPVerb(string $verb) { - $this->HTTPVerb = $verb; + $this->HTTPVerb = strtolower($verb); return $this; } diff --git a/system/Router/Router.php b/system/Router/Router.php index 8dd35c65..f9299fa1 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -126,7 +126,7 @@ public function __construct(RouteCollectionInterface $routes, ?Request $request $this->controller = $this->collection->getDefaultController(); $this->method = $this->collection->getDefaultMethod(); - $this->collection->setHTTPVerb(strtolower($request->getMethod() ?? $_SERVER['REQUEST_METHOD'])); + $this->collection->setHTTPVerb($request->getMethod() ?? $_SERVER['REQUEST_METHOD']); $this->translateURIDashes = $this->collection->shouldTranslateURIDashes(); @@ -504,7 +504,7 @@ protected function checkRoutes(string $uri): bool public function autoRoute(string $uri) { [$this->directory, $this->controller, $this->method, $this->params] - = $this->autoRouter->getRoute($uri); + = $this->autoRouter->getRoute($uri, $this->collection->getHTTPVerb()); } /** diff --git a/system/Test/FeatureTestCase.php b/system/Test/FeatureTestCase.php index 2c2233df..bf157aea 100644 --- a/system/Test/FeatureTestCase.php +++ b/system/Test/FeatureTestCase.php @@ -148,9 +148,6 @@ public function skipEvents() * instance that can be used to run many assertions against. * * @return FeatureResponse - * - * @throws Exception - * @throws RedirectException */ public function call(string $method, string $path, ?array $params = null) { diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index e6e8492e..f7a6d060 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -138,9 +138,6 @@ public function skipEvents() * instance that can be used to run many assertions against. * * @return TestResponse - * - * @throws RedirectException - * @throws Exception */ public function call(string $method, string $path, ?array $params = null) { @@ -175,6 +172,9 @@ public function call(string $method, string $path, ?array $params = null) // Make sure filters are reset between tests Services::injectMock('filters', Services::filters(null, false)); + // Make sure validation is reset between tests + Services::injectMock('validation', Services::validation(null, false)); + $response = $this->app ->setContext('web') ->setRequest($request) diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 93b86ea6..1bb2c243 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -263,7 +263,6 @@ public function required_with($str = null, ?string $fields = null, array $data = // If the field is present we can safely assume that // the field is here, no matter whether the corresponding // search field is present or not. - $fields = explode(',', $fields); $present = $this->required($str ?? ''); if ($present) { @@ -272,11 +271,14 @@ public function required_with($str = null, ?string $fields = null, array $data = // Still here? Then we fail this test if // any of the fields are present in $data - // as $fields is the lis + // as $fields is the list $requiredFields = []; - foreach ($fields as $field) { - if ((array_key_exists($field, $data) && ! empty($data[$field])) || (strpos($field, '.') !== false && ! empty(dot_array_search($field, $data)))) { + foreach (explode(',', $fields) as $field) { + if ( + (array_key_exists($field, $data) && ! empty($data[$field])) + || (strpos($field, '.') !== false && ! empty(dot_array_search($field, $data))) + ) { $requiredFields[] = $field; } } @@ -285,7 +287,7 @@ public function required_with($str = null, ?string $fields = null, array $data = } /** - * The field is required when all of the other fields are present + * The field is required when all the other fields are present * in the data but not required. * * Example (field is required when the id or email field is missing): @@ -296,8 +298,13 @@ public function required_with($str = null, ?string $fields = null, array $data = * @param string|null $otherFields The param fields of required_without[]. * @param string|null $field This rule param fields aren't present, this field is required. */ - public function required_without($str = null, ?string $otherFields = null, array $data = [], ?string $error = null, ?string $field = null): bool - { + public function required_without( + $str = null, + ?string $otherFields = null, + array $data = [], + ?string $error = null, + ?string $field = null + ): bool { if ($otherFields === null || empty($data)) { throw new InvalidArgumentException('You must supply the parameters: otherFields, data.'); } @@ -305,8 +312,7 @@ public function required_without($str = null, ?string $otherFields = null, array // If the field is present we can safely assume that // the field is here, no matter whether the corresponding // search field is present or not. - $otherFields = explode(',', $otherFields); - $present = $this->required($str ?? ''); + $present = $this->required($str ?? ''); if ($present) { return true; @@ -314,10 +320,14 @@ public function required_without($str = null, ?string $otherFields = null, array // Still here? Then we fail this test if // any of the fields are not present in $data - foreach ($otherFields as $otherField) { - if ((strpos($otherField, '.') === false) && (! array_key_exists($otherField, $data) || empty($data[$otherField]))) { + foreach (explode(',', $otherFields) as $otherField) { + if ( + (strpos($otherField, '.') === false) + && (! array_key_exists($otherField, $data) || empty($data[$otherField])) + ) { return false; } + if (strpos($otherField, '.') !== false) { if ($field === null) { throw new InvalidArgumentException('You must supply the parameters: field.'); diff --git a/system/Validation/StrictRules/Rules.php b/system/Validation/StrictRules/Rules.php index f95bbe7d..cdc20f5e 100644 --- a/system/Validation/StrictRules/Rules.php +++ b/system/Validation/StrictRules/Rules.php @@ -347,7 +347,7 @@ public function required_with($str = null, ?string $fields = null, array $data = } /** - * The field is required when all of the other fields are present + * The field is required when all the other fields are present * in the data but not required. * * Example (field is required when the id or email field is missing): @@ -358,8 +358,13 @@ public function required_with($str = null, ?string $fields = null, array $data = * @param string|null $otherFields The param fields of required_without[]. * @param string|null $field This rule param fields aren't present, this field is required. */ - public function required_without($str = null, ?string $otherFields = null, array $data = [], ?string $error = null, ?string $field = null): bool - { + public function required_without( + $str = null, + ?string $otherFields = null, + array $data = [], + ?string $error = null, + ?string $field = null + ): bool { return $this->nonStrictRules->required_without($str, $otherFields, $data, $error, $field); } } diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 9b77b3c8..5e539561 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -116,12 +116,15 @@ public function __construct($config, RendererInterface $view) * @param array|null $data The array of data to validate. * @param string|null $group The predefined group of rules to apply. * @param string|null $dbGroup The database group to use. + * + * @TODO Type ?string for $dbGroup should be removed. + * See https://github.com/codeigniter4/CodeIgniter4/issues/6723 */ public function run(?array $data = null, ?string $group = null, ?string $dbGroup = null): bool { $data ??= $this->data; - // i.e. is_unique + // `DBGroup` is a reserved name. For is_unique and is_not_unique $data['DBGroup'] = $dbGroup; $this->loadRuleSets(); @@ -184,17 +187,28 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup } /** - * Runs the validation process, returning true or false - * determining whether validation was successful or not. + * Runs the validation process, returning true or false determining whether + * validation was successful or not. * * @param array|bool|float|int|object|string|null $value + * @param array|string $rules * @param string[] $errors + * @param string|null $dbGroup The database group to use. */ - public function check($value, string $rule, array $errors = []): bool + public function check($value, $rules, array $errors = [], $dbGroup = null): bool { $this->reset(); - return $this->setRule('check', null, $rule, $errors)->run(['check' => $value]); + return $this->setRule( + 'check', + null, + $rules, + $errors + )->run( + ['check' => $value], + null, + $dbGroup + ); } /** @@ -204,87 +218,30 @@ public function check($value, string $rule, array $errors = []): bool * so that we can collect all of the first errors. * * @param array|string $value - * @param array|null $rules - * @param array|null $data The array of data to validate, with `DBGroup`. + * @param array $rules + * @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, + $rules = null, // @TODO remove `= null` + ?array $data = null, // @TODO remove `= null` ?string $originalField = null ): bool { if ($data === null) { throw new InvalidArgumentException('You must supply the parameter: data.'); } - if (in_array('if_exist', $rules, true)) { - $flattenedData = array_flatten_with_dots($data); - $ifExistField = $field; - - if (strpos($field, '.*') !== false) { - // We'll change the dot notation into a PCRE pattern that can be used later - $ifExistField = str_replace('\.\*', '\.(?:[^\.]+)', preg_quote($field, '/')); - $dataIsExisting = false; - $pattern = sprintf('/%s/u', $ifExistField); - - foreach (array_keys($flattenedData) as $item) { - if (preg_match($pattern, $item) === 1) { - $dataIsExisting = true; - break; - } - } - } else { - $dataIsExisting = array_key_exists($ifExistField, $flattenedData); - } - - unset($ifExistField, $flattenedData); - - if (! $dataIsExisting) { - // we return early if `if_exist` is not satisfied. we have nothing to do here. - return true; - } - - // Otherwise remove the if_exist rule and continue the process - $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'if_exist'); + $rules = $this->processIfExist($field, $rules, $data); + if ($rules === true) { + return true; } - if (in_array('permit_empty', $rules, true)) { - if ( - ! in_array('required', $rules, true) - && (is_array($value) ? $value === [] : trim((string) $value) === '') - ) { - $passed = true; - - foreach ($rules as $rule) { - if (! $this->isClosure($rule) && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) { - $rule = $match[1]; - $param = $match[2]; - - if (! in_array($rule, ['required_with', 'required_without'], true)) { - continue; - } - - // Check in our rulesets - foreach ($this->ruleSetInstances as $set) { - if (! method_exists($set, $rule)) { - continue; - } - - $passed = $passed && $set->{$rule}($value, $param, $data); - break; - } - } - } - - if ($passed === true) { - return true; - } - } - - $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'permit_empty'); + $rules = $this->processPermitEmpty($value, $rules, $data); + if ($rules === true) { + return true; } foreach ($rules as $i => $rule) { @@ -360,6 +317,92 @@ protected function processRules( return true; } + /** + * @param array $data The array of data to validate, with `DBGroup`. + * + * @return array|true The modified rules or true if we return early + */ + private function processIfExist(string $field, array $rules, array $data) + { + if (in_array('if_exist', $rules, true)) { + $flattenedData = array_flatten_with_dots($data); + $ifExistField = $field; + + if (strpos($field, '.*') !== false) { + // We'll change the dot notation into a PCRE pattern that can be used later + $ifExistField = str_replace('\.\*', '\.(?:[^\.]+)', preg_quote($field, '/')); + $dataIsExisting = false; + $pattern = sprintf('/%s/u', $ifExistField); + + foreach (array_keys($flattenedData) as $item) { + if (preg_match($pattern, $item) === 1) { + $dataIsExisting = true; + break; + } + } + } else { + $dataIsExisting = array_key_exists($ifExistField, $flattenedData); + } + + if (! $dataIsExisting) { + // we return early if `if_exist` is not satisfied. we have nothing to do here. + return true; + } + + // Otherwise remove the if_exist rule and continue the process + $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'if_exist'); + } + + return $rules; + } + + /** + * @param array|string $value + * @param array $data The array of data to validate, with `DBGroup`. + * + * @return array|true The modified rules or true if we return early + */ + private function processPermitEmpty($value, array $rules, array $data) + { + if (in_array('permit_empty', $rules, true)) { + if ( + ! in_array('required', $rules, true) + && (is_array($value) ? $value === [] : trim((string) $value) === '') + ) { + $passed = true; + + foreach ($rules as $rule) { + if (! $this->isClosure($rule) && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) { + $rule = $match[1]; + $param = $match[2]; + + if (! in_array($rule, ['required_with', 'required_without'], true)) { + continue; + } + + // Check in our rulesets + foreach ($this->ruleSetInstances as $set) { + if (! method_exists($set, $rule)) { + continue; + } + + $passed = $passed && $set->{$rule}($value, $param, $data); + break; + } + } + } + + if ($passed === true) { + return true; + } + } + + $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'permit_empty'); + } + + return $rules; + } + /** * @param Closure|string $rule */ @@ -679,6 +722,7 @@ protected function fillPlaceholders(array $rules, array $data): array foreach ($placeholderFields as $field) { $validator ??= Services::validation(null, false); + assert($validator instanceof Validation); $placeholderRules = $rules[$field]['rules'] ?? null; @@ -699,7 +743,8 @@ protected function fillPlaceholders(array $rules, array $data): array } // Validate the placeholder field - if (! $validator->check($data[$field], implode('|', $placeholderRules))) { + $dbGroup = $data['DBGroup'] ?? null; + if (! $validator->check($data[$field], $placeholderRules, [], $dbGroup)) { // if fails, do nothing continue; } diff --git a/system/Validation/ValidationInterface.php b/system/Validation/ValidationInterface.php index 8c018b9a..9336c26e 100644 --- a/system/Validation/ValidationInterface.php +++ b/system/Validation/ValidationInterface.php @@ -32,12 +32,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup * Check; runs the validation process, returning true or false * determining whether or not validation was successful. * - * @param array|bool|float|int|object|string|null $value Value to validate. + * @param array|bool|float|int|object|string|null $value Value to validate. + * @param array|string $rules * @param string[] $errors + * @param string|null $dbGroup The database group to use. * * @return bool True if valid, else false. */ - public function check($value, string $rule, array $errors = []): bool; + public function check($value, $rules, array $errors = [], $dbGroup = null): bool; /** * Takes a Request object and grabs the input data to use from its diff --git a/system/View/Cell.php b/system/View/Cell.php index 2e0fc22b..b898adc0 100644 --- a/system/View/Cell.php +++ b/system/View/Cell.php @@ -175,9 +175,9 @@ protected function determineClass(string $library): array } // locate and return an instance of the cell - $class = Factories::cells($class); + $object = Factories::cells($class); - if (! is_object($class)) { + if (! is_object($object)) { throw ViewException::forInvalidCellClass($class); } @@ -186,7 +186,7 @@ protected function determineClass(string $library): array } return [ - $class, + $object, $method, ]; } diff --git a/system/View/View.php b/system/View/View.php index 9a391047..164d6764 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -171,20 +171,27 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n // multiple views are called in a view, it won't // clean it unless we mean it to. $saveData ??= $this->saveData; - $fileExt = pathinfo($view, PATHINFO_EXTENSION); - $realPath = empty($fileExt) ? $view . '.php' : $view; // allow Views as .html, .tpl, etc (from CI3) - $this->renderVars['view'] = $realPath; + + $fileExt = pathinfo($view, PATHINFO_EXTENSION); + // allow Views as .html, .tpl, etc (from CI3) + $this->renderVars['view'] = empty($fileExt) ? $view . '.php' : $view; + $this->renderVars['options'] = $options ?? []; // Was it cached? if (isset($this->renderVars['options']['cache'])) { - $cacheName = $this->renderVars['options']['cache_name'] ?? str_replace('.php', '', $this->renderVars['view']); + $cacheName = $this->renderVars['options']['cache_name'] + ?? str_replace('.php', '', $this->renderVars['view']); $cacheName = str_replace(['\\', '/'], '', $cacheName); $this->renderVars['cacheName'] = $cacheName; if ($output = cache($this->renderVars['cacheName'])) { - $this->logPerformance($this->renderVars['start'], microtime(true), $this->renderVars['view']); + $this->logPerformance( + $this->renderVars['start'], + microtime(true), + $this->renderVars['view'] + ); return $output; } @@ -193,7 +200,11 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $this->renderVars['file'] = $this->viewPath . $this->renderVars['view']; if (! is_file($this->renderVars['file'])) { - $this->renderVars['file'] = $this->loader->locateFile($this->renderVars['view'], 'Views', empty($fileExt) ? 'php' : $fileExt); + $this->renderVars['file'] = $this->loader->locateFile( + $this->renderVars['view'], + 'Views', + empty($fileExt) ? 'php' : $fileExt + ); } // locateFile will return an empty string if the file cannot be found. @@ -233,10 +244,16 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $output = $this->decorateOutput($output); - $this->logPerformance($this->renderVars['start'], microtime(true), $this->renderVars['view']); + $this->logPerformance( + $this->renderVars['start'], + microtime(true), + $this->renderVars['view'] + ); - if (($this->debug && (! isset($options['debug']) || $options['debug'] === true)) - && in_array(DebugToolbar::class, service('filters')->getFiltersClass()['after'], true) + $afterFilters = service('filters')->getFiltersClass()['after']; + if ( + ($this->debug && (! isset($options['debug']) || $options['debug'] === true)) + && in_array(DebugToolbar::class, $afterFilters, true) ) { $toolbarCollectors = config(Toolbar::class)->collectors; @@ -253,7 +270,11 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n // Should we cache? if (isset($this->renderVars['options']['cache'])) { - cache()->save($this->renderVars['cacheName'], $output, (int) $this->renderVars['options']['cache']); + cache()->save( + $this->renderVars['cacheName'], + $output, + (int) $this->renderVars['options']['cache'] + ); } $this->tempData = null;