From 1cbdeac15c34e7bb85a70906324cb2195edba452 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Sat, 4 Sep 2021 22:19:17 +0800 Subject: [PATCH 001/490] Fixes and enhancements to Exceptions --- system/Debug/Exceptions.php | 101 ++++++++++++-------------- tests/system/Debug/ExceptionsTest.php | 53 ++++++++++++-- 2 files changed, 91 insertions(+), 63 deletions(-) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 5950148984ef..04556c5c4f20 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -19,7 +19,6 @@ use Config\Paths; use ErrorException; use Throwable; -use function error_reporting; /** * Exceptions manager @@ -64,17 +63,11 @@ class Exceptions */ protected $response; - /** - * Constructor. - */ public function __construct(ExceptionsConfig $config, IncomingRequest $request, Response $response) { $this->ob_level = ob_get_level(); - $this->viewPath = rtrim($config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR; - - $this->config = $config; - + $this->config = $config; $this->request = $request; $this->response = $response; } @@ -82,17 +75,13 @@ public function __construct(ExceptionsConfig $config, IncomingRequest $request, /** * Responsible for registering the error, exception and shutdown * handling of our application. + * + * @codeCoverageIgnore */ public function initialize() { - // Set the Exception Handler set_exception_handler([$this, 'exceptionHandler']); - - // Set the Error Handler set_error_handler([$this, 'errorHandler']); - - // Set the handler for shutdown to catch Parse errors - // Do we need this in PHP7? register_shutdown_function([$this, 'shutdownHandler']); } @@ -105,12 +94,8 @@ public function initialize() */ public function exceptionHandler(Throwable $exception) { - [ - $statusCode, - $exitCode, - ] = $this->determineCodes($exception); + [$statusCode, $exitCode] = $this->determineCodes($exception); - // Log it if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { log_message('critical', $exception->getMessage() . "\n{trace}", [ 'trace' => $exception->getTraceAsString(), @@ -119,8 +104,7 @@ public function exceptionHandler(Throwable $exception) if (! is_cli()) { $this->response->setStatusCode($statusCode); - $header = "HTTP/{$this->request->getProtocolVersion()} {$this->response->getStatusCode()} {$this->response->getReason()}"; - header($header, true, $statusCode); + header(sprintf('HTTP/%s %s %s', $this->request->getProtocolVersion(), $this->response->getStatusCode(), $this->response->getReasonPhrase()), true, $statusCode); if (strpos($this->request->getHeaderLine('accept'), 'text/html') === false) { $this->respond(ENVIRONMENT === 'development' ? $this->collectVars($exception, $statusCode) : '', $statusCode)->send(); @@ -142,6 +126,8 @@ public function exceptionHandler(Throwable $exception) * This seems to be primarily when a user triggers it with trigger_error(). * * @throws ErrorException + * + * @codeCoverageIgnore */ public function errorHandler(int $severity, string $message, ?string $file = null, ?int $line = null) { @@ -149,24 +135,27 @@ public function errorHandler(int $severity, string $message, ?string $file = nul return; } - // Convert it to an exception and pass it along. throw new ErrorException($message, 0, $severity, $file, $line); } /** * Checks to see if any errors have happened during shutdown that * need to be caught and handle them. + * + * @codeCoverageIgnore */ public function shutdownHandler() { $error = error_get_last(); - // If we've got an error that hasn't been displayed, then convert - // it to an Exception and use the Exception handler to display it - // to the user. - // Fatal Error? - if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE], true)) { - $this->exceptionHandler(new ErrorException($error['message'], $error['type'], 0, $error['file'], $error['line'])); + if ($error === null) { + return; + } + + ['type' => $type, 'message' => $message, 'file' => $file, 'line' => $line] = $error; + + if (in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE], true)) { + $this->exceptionHandler(new ErrorException($message, $type, 0, $file, $line)); } } @@ -222,20 +211,25 @@ protected function render(Throwable $exception, int $statusCode) $viewFile = $altPath . $altView; } - // Prepare the vars - $vars = $this->collectVars($exception, $statusCode); - extract($vars); + if (! isset($viewFile)) { + echo 'The error view files were not found. Cannot render exception trace.'; + + exit(1); + } - // Render it if (ob_get_level() > $this->ob_level + 1) { ob_end_clean(); } - ob_start(); - include $viewFile; // @phpstan-ignore-line - $buffer = ob_get_contents(); - ob_end_clean(); - echo $buffer; + echo(function () use ($exception, $statusCode, $viewFile): string { + $vars = $this->collectVars($exception, $statusCode); + extract($vars, EXTR_SKIP); + + ob_start(); + include $viewFile; + + return ob_get_clean(); + })(); } /** @@ -244,7 +238,8 @@ protected function render(Throwable $exception, int $statusCode) protected function collectVars(Throwable $exception, int $statusCode): array { $trace = $exception->getTrace(); - if (! empty($this->config->sensitiveDataInTrace)) { + + if ($this->config->sensitiveDataInTrace !== []) { $this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace); } @@ -279,11 +274,11 @@ protected function maskSensitiveData(&$trace, array $keysToMask, string $path = } } - if (! is_iterable($trace) && is_object($trace)) { + if (is_object($trace)) { $trace = get_object_vars($trace); } - if (is_iterable($trace)) { + if (is_array($trace)) { foreach ($trace as $pathKey => $subarray) { $this->maskSensitiveData($subarray, $keysToMask, $path . '/' . $pathKey); } @@ -298,19 +293,18 @@ protected function determineCodes(Throwable $exception): array $statusCode = abs($exception->getCode()); if ($statusCode < 100 || $statusCode > 599) { - $exitStatus = $statusCode + EXIT__AUTO_MIN; // 9 is EXIT__AUTO_MIN - if ($exitStatus > EXIT__AUTO_MAX) { // 125 is EXIT__AUTO_MAX - $exitStatus = EXIT_ERROR; // EXIT_ERROR + $exitStatus = $statusCode + EXIT__AUTO_MIN; + + if ($exitStatus > EXIT__AUTO_MAX) { + $exitStatus = EXIT_ERROR; } + $statusCode = 500; } else { - $exitStatus = 1; // EXIT_ERROR + $exitStatus = EXIT_ERROR; } - return [ - $statusCode ?: 500, - $exitStatus, - ]; + return [$statusCode, $exitStatus]; } //-------------------------------------------------------------------- @@ -318,8 +312,6 @@ protected function determineCodes(Throwable $exception): array //-------------------------------------------------------------------- /** - * Clean Path - * * This makes nicer looking paths for the error output. */ public static function cleanPath(string $file): string @@ -354,6 +346,7 @@ public static function describeMemory(int $bytes): string if ($bytes < 1024) { return $bytes . 'B'; } + if ($bytes < 1048576) { return round($bytes / 1024, 2) . 'KB'; } @@ -390,18 +383,16 @@ public static function highlightFile(string $file, int $lineNumber, int $lines = $source = str_replace(["\r\n", "\r"], "\n", $source); $source = explode("\n", highlight_string($source, true)); $source = str_replace('
', "\n", $source[1]); - $source = explode("\n", str_replace("\r\n", "\n", $source)); // Get just the part to show - $start = $lineNumber - (int) round($lines / 2); - $start = $start < 0 ? 0 : $start; + $start = max($lineNumber - (int) round($lines / 2), 0); // Get just the lines we need to display, while keeping line numbers... $source = array_splice($source, $start, $lines, true); // @phpstan-ignore-line // Used to format the line number in the source - $format = '% ' . strlen(sprintf('%s', $start + $lines)) . 'd'; + $format = '% ' . strlen((string) ($start + $lines)) . 'd'; $out = ''; // Because the highlighting may have an uneven number @@ -412,11 +403,11 @@ public static function highlightFile(string $file, int $lineNumber, int $lines = foreach ($source as $n => $row) { $spans += substr_count($row, ']+>#', $row, $tags); + $out .= sprintf( "{$format} %s\n%s", $n + $start + 1, diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php index 115ae335c5e1..9bfcbb93c08b 100644 --- a/tests/system/Debug/ExceptionsTest.php +++ b/tests/system/Debug/ExceptionsTest.php @@ -11,27 +11,64 @@ namespace CodeIgniter\Debug; +use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\ReflectionHelper; +use Config\Exceptions as ExceptionsConfig; use Config\Services; +use RuntimeException; /** * @internal */ final class ExceptionsTest extends CIUnitTestCase { - public function testNew() + use ReflectionHelper; + + /** + * @var Exceptions + */ + private $exception; + + protected function setUp(): void { - $actual = new Exceptions(new \Config\Exceptions(), Services::request(), Services::response()); - $this->assertInstanceOf(Exceptions::class, $actual); + $this->exception = new Exceptions(new ExceptionsConfig(), Services::request(), Services::response()); + } + + public function testDetermineViews(): void + { + $determineView = $this->getPrivateMethodInvoker($this->exception, 'determineView'); + + $this->assertSame('error_404.php', $determineView(PageNotFoundException::forControllerNotFound('Foo', 'bar'), '')); + $this->assertSame('error_exception.php', $determineView(new RuntimeException('Exception'), '')); + $this->assertSame('error_404.php', $determineView(new RuntimeException('foo', 404), 'app/Views/errors/cli')); + } + + public function testCollectVars(): void + { + $vars = $this->getPrivateMethodInvoker($this->exception, 'collectVars')(new RuntimeException('This.'), 404); + + $this->assertIsArray($vars); + $this->assertCount(7, $vars); + + foreach (['title', 'type', 'code', 'message', 'file', 'line', 'trace'] as $key) { + $this->assertArrayHasKey($key, $vars); + } + } + + public function testDetermineCodes(): void + { + $determineCodes = $this->getPrivateMethodInvoker($this->exception, 'determineCodes'); + + $this->assertSame([500, 9], $determineCodes(new RuntimeException('This.'))); + $this->assertSame([500, 1], $determineCodes(new RuntimeException('That.', 600))); + $this->assertSame([404, 1], $determineCodes(new RuntimeException('There.', 404))); } /** * @dataProvider dirtyPathsProvider - * - * @param mixed $file - * @param mixed $expected */ - public function testCleanPaths($file, $expected) + public function testCleanPaths(string $file, string $expected): void { $this->assertSame($expected, Exceptions::cleanPath($file)); } @@ -40,7 +77,7 @@ public function dirtyPathsProvider() { $ds = DIRECTORY_SEPARATOR; - return [ + yield from [ [ APPPATH . 'Config' . $ds . 'App.php', 'APPPATH' . $ds . 'Config' . $ds . 'App.php', From a355c3bf40c9cc82f618d9a0aea5373be3692415 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Sat, 4 Sep 2021 22:20:36 +0800 Subject: [PATCH 002/490] Add entry to changelog for initial PHP 8.1 changes --- user_guide_src/source/changelogs/v4.1.4.rst | 67 +++++++++++++-------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.1.4.rst b/user_guide_src/source/changelogs/v4.1.4.rst index f5f7fb7d33b9..daca6b616294 100644 --- a/user_guide_src/source/changelogs/v4.1.4.rst +++ b/user_guide_src/source/changelogs/v4.1.4.rst @@ -7,28 +7,45 @@ Release Date: Not released Breaking Changes: -The following methods were changed from "public" to "protected" to match their parent class methods and better align with their uses: - -* CodeIgniter4\Database\MySQLi\Connection::execute() -* CodeIgniter4\Database\MySQLi\Connection::_fieldData() -* CodeIgniter4\Database\MySQLi\Connection::_indexData() -* CodeIgniter4\Database\MySQLi\Connection::_foreignKeyData() -* CodeIgniter4\Database\Postgre\Builder::_like_statement() -* CodeIgniter4\Database\Postgre\Connection::execute() -* CodeIgniter4\Database\Postgre\Connection::_fieldData() -* CodeIgniter4\Database\Postgre\Connection::_indexData() -* CodeIgniter4\Database\Postgre\Connection::_foreignKeyData() -* CodeIgniter4\Database\SQLSRV\Connection::execute() -* CodeIgniter4\Database\SQLSRV\Connection::_fieldData() -* CodeIgniter4\Database\SQLSRV\Connection::_indexData() -* CodeIgniter4\Database\SQLSRV\Connection::_foreignKeyData() -* CodeIgniter4\Database\SQLite3\Connection::execute() -* CodeIgniter4\Database\SQLite3\Connection::_fieldData() -* CodeIgniter4\Database\SQLite3\Connection::_indexData() -* CodeIgniter4\Database\SQLite3\Connection::_foreignKeyData() -* CodeIgniter4\Images\Handlers\GDHandler::_flatten() -* CodeIgniter4\Images\Handlers\GDHandler::_flip() -* CodeIgniter4\Images\Handlers\ImageMagickHandler::_flatten() -* CodeIgniter4\Images\Handlers\ImageMagickHandler::_flip() -* CodeIgniter4\Test\Mock\MockIncomingRequest::detectURI() -* CodeIgniter4\Test\Mock\MockSecurity.php::sendCookie() +- The following methods were changed from "public" to "protected" to match their parent class methods and better align with their uses: + +* ``CodeIgniter\Database\MySQLi\Connection::execute()`` +* ``CodeIgniter\Database\MySQLi\Connection::_fieldData()`` +* ``CodeIgniter\Database\MySQLi\Connection::_indexData()`` +* ``CodeIgniter\Database\MySQLi\Connection::_foreignKeyData()`` +* ``CodeIgniter\Database\Postgre\Builder::_like_statement()`` +* ``CodeIgniter\Database\Postgre\Connection::execute()`` +* ``CodeIgniter\Database\Postgre\Connection::_fieldData()`` +* ``CodeIgniter\Database\Postgre\Connection::_indexData()`` +* ``CodeIgniter\Database\Postgre\Connection::_foreignKeyData()`` +* ``CodeIgniter\Database\SQLSRV\Connection::execute()`` +* ``CodeIgniter\Database\SQLSRV\Connection::_fieldData()`` +* ``CodeIgniter\Database\SQLSRV\Connection::_indexData()`` +* ``CodeIgniter\Database\SQLSRV\Connection::_foreignKeyData()`` +* ``CodeIgniter\Database\SQLite3\Connection::execute()`` +* ``CodeIgniter\Database\SQLite3\Connection::_fieldData()`` +* ``CodeIgniter\Database\SQLite3\Connection::_indexData()`` +* ``CodeIgniter\Database\SQLite3\Connection::_foreignKeyData()`` +* ``CodeIgniter\Images\Handlers\GDHandler::_flatten()`` +* ``CodeIgniter\Images\Handlers\GDHandler::_flip()`` +* ``CodeIgniter\Images\Handlers\ImageMagickHandler::_flatten()`` +* ``CodeIgniter\Images\Handlers\ImageMagickHandler::_flip()`` +* ``CodeIgniter\Test\Mock\MockIncomingRequest::detectURI()`` +* ``CodeIgniter\Test\Mock\MockSecurity.php::sendCookie()`` + +- To be compatible with the strict inheritance checks of PHP 8.1, the following method signatures were added return types to match their parents' signatures whenever possible: + +* ``CodeIgniter\Cookie\Cookie::offsetExists()`` +* ``CodeIgniter\Cookie\Cookie::offsetSet()`` +* ``CodeIgniter\Cookie\Cookie::offsetUnset()`` +* ``CodeIgniter\Cookie\CookieStore::getIterator()`` +* ``CodeIgniter\I18n\Time::__wakeup()`` +* ``CodeIgniter\Test\Filters\CITestStreamFilter::filter()`` + +- Related to the strict inheritance checks of PHP 8.1, the following session handlers implementing ``SessionHandlerInterface`` have their public methods modified to match the interface: + +* ``CodeIgniter\Session\Handlers\ArrayHandler`` +* ``CodeIgniter\Session\Handlers\DatabaseHandler`` +* ``CodeIgniter\Session\Handlers\FileHandler`` +* ``CodeIgniter\Session\Handlers\MemcachedHandler`` +* ``CodeIgniter\Session\Handlers\RedisHandler`` From 8207b2f3f2bdddbb9e99813053303f210ad68d5c Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Sun, 5 Sep 2021 20:05:46 +0800 Subject: [PATCH 003/490] Fix entity name generation when bundled in model --- system/Commands/Generators/ModelGenerator.php | 22 ++++--- .../Commands/Generators/Views/entity.tpl.php | 6 +- .../Commands/Generators/Views/model.tpl.php | 46 +++++++-------- tests/system/Commands/ModelGeneratorTest.php | 57 ++++++++++++++----- 4 files changed, 80 insertions(+), 51 deletions(-) diff --git a/system/Commands/Generators/ModelGenerator.php b/system/Commands/Generators/ModelGenerator.php index 1ad8a75e1f8c..b4b7ffcda2ca 100644 --- a/system/Commands/Generators/ModelGenerator.php +++ b/system/Commands/Generators/ModelGenerator.php @@ -92,14 +92,17 @@ public function run(array $params) protected function prepare(string $class): string { $table = $this->getOption('table'); - $DBGroup = $this->getOption('dbgroup'); + $dbGroup = $this->getOption('dbgroup'); $return = $this->getOption('return'); - $baseClass = strtolower(str_replace(trim(implode('\\', array_slice(explode('\\', $class), 0, -1)), '\\') . '\\', '', $class)); - $baseClass = strpos($baseClass, 'model') ? str_replace('model', '', $baseClass) : $baseClass; + $baseClass = class_basename($class); - $table = is_string($table) ? $table : plural($baseClass); - $DBGroup = is_string($DBGroup) ? $DBGroup : 'default'; + if (preg_match('/^(\S+)Model$/i', $baseClass, $match) === 1) { + $baseClass = $match[1]; + } + + $table = is_string($table) ? $table : plural(strtolower($baseClass)); + $dbGroup = is_string($dbGroup) ? $dbGroup : 'default'; $return = is_string($return) ? $return : 'array'; if (! in_array($return, ['array', 'object', 'entity'], true)) { @@ -112,17 +115,20 @@ protected function prepare(string $class): string if ($return === 'entity') { $return = str_replace('Models', 'Entities', $class); - if ($pos = strpos($return, 'Model')) { - $return = substr($return, 0, $pos); + if (preg_match('/^(\S+)Model$/i', $return, $match) === 1) { + $return = $match[1]; if ($this->getOption('suffix')) { $return .= 'Entity'; } } + $return = '\\' . trim($return, '\\') . '::class'; $this->call('make:entity', array_merge([$baseClass], $this->params)); + } else { + $return = "'{$return}'"; } - return $this->parseTemplate($class, ['{table}', '{DBGroup}', '{return}'], [$table, $DBGroup, $return]); + return $this->parseTemplate($class, ['{table}', '{dbGroup}', '{return}'], [$table, $dbGroup, $return]); } } diff --git a/system/Commands/Generators/Views/entity.tpl.php b/system/Commands/Generators/Views/entity.tpl.php index 6623e2f14bbb..c74c776f4ad3 100644 --- a/system/Commands/Generators/Views/entity.tpl.php +++ b/system/Commands/Generators/Views/entity.tpl.php @@ -7,10 +7,6 @@ class {class} extends Entity { protected $datamap = []; - protected $dates = [ - 'created_at', - 'updated_at', - 'deleted_at', - ]; + protected $dates = ['created_at', 'updated_at', 'deleted_at']; protected $casts = []; } diff --git a/system/Commands/Generators/Views/model.tpl.php b/system/Commands/Generators/Views/model.tpl.php index 088ecb7e1161..5fc5ed9ba428 100644 --- a/system/Commands/Generators/Views/model.tpl.php +++ b/system/Commands/Generators/Views/model.tpl.php @@ -6,22 +6,22 @@ class {class} extends Model { - protected $DBGroup = '{DBGroup}'; - protected $table = '{table}'; - protected $primaryKey = 'id'; - protected $useAutoIncrement = true; - protected $insertID = 0; - protected $returnType = '{return}'; - protected $useSoftDeletes = false; - protected $protectFields = true; - protected $allowedFields = []; + protected $DBGroup = '{dbGroup}'; + protected $table = '{table}'; + protected $primaryKey = 'id'; + protected $useAutoIncrement = true; + protected $insertID = 0; + protected $returnType = {return}; + protected $useSoftDeletes = false; + protected $protectFields = true; + protected $allowedFields = []; // Dates - protected $useTimestamps = false; - protected $dateFormat = 'datetime'; - protected $createdField = 'created_at'; - protected $updatedField = 'updated_at'; - protected $deletedField = 'deleted_at'; + protected $useTimestamps = false; + protected $dateFormat = 'datetime'; + protected $createdField = 'created_at'; + protected $updatedField = 'updated_at'; + protected $deletedField = 'deleted_at'; // Validation protected $validationRules = []; @@ -30,13 +30,13 @@ class {class} extends Model protected $cleanValidationRules = true; // Callbacks - protected $allowCallbacks = true; - protected $beforeInsert = []; - protected $afterInsert = []; - protected $beforeUpdate = []; - protected $afterUpdate = []; - protected $beforeFind = []; - protected $afterFind = []; - protected $beforeDelete = []; - protected $afterDelete = []; + protected $allowCallbacks = true; + protected $beforeInsert = []; + protected $afterInsert = []; + protected $beforeUpdate = []; + protected $afterUpdate = []; + protected $beforeFind = []; + protected $afterFind = []; + protected $beforeDelete = []; + protected $afterDelete = []; } diff --git a/tests/system/Commands/ModelGeneratorTest.php b/tests/system/Commands/ModelGeneratorTest.php index 68e001103fb5..2d00460c53ee 100644 --- a/tests/system/Commands/ModelGeneratorTest.php +++ b/tests/system/Commands/ModelGeneratorTest.php @@ -19,10 +19,11 @@ */ final class ModelGeneratorTest extends CIUnitTestCase { - protected $streamFilter; + private $streamFilter; protected function setUp(): void { + parent::setUp(); CITestStreamFilter::$buffer = ''; $this->streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); @@ -31,16 +32,18 @@ protected function setUp(): void protected function tearDown(): void { + parent::tearDown(); stream_filter_remove($this->streamFilter); $result = str_replace(["\033[0;32m", "\033[0m", "\n"], '', CITestStreamFilter::$buffer); $file = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, trim(substr($result, 14))); + if (is_file($file)) { unlink($file); } } - protected function getFileContent(string $filepath): string + private function getFileContent(string $filepath): string { if (! is_file($filepath)) { return ''; @@ -51,14 +54,14 @@ protected function getFileContent(string $filepath): string public function testGenerateModel() { - command('make:model user -table users'); + command('make:model user --table users'); $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer); $file = APPPATH . 'Models/User.php'; $this->assertFileExists($file); $this->assertStringContainsString('extends Model', $this->getFileContent($file)); - $this->assertStringContainsString('protected $table = \'users\';', $this->getFileContent($file)); - $this->assertStringContainsString('protected $DBGroup = \'default\';', $this->getFileContent($file)); - $this->assertStringContainsString('protected $returnType = \'array\';', $this->getFileContent($file)); + $this->assertStringContainsString('protected $table = \'users\';', $this->getFileContent($file)); + $this->assertStringContainsString('protected $DBGroup = \'default\';', $this->getFileContent($file)); + $this->assertStringContainsString('protected $returnType = \'array\';', $this->getFileContent($file)); } public function testGenerateModelWithOptionTable() @@ -67,7 +70,7 @@ public function testGenerateModelWithOptionTable() $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer); $file = APPPATH . 'Models/Cars.php'; $this->assertFileExists($file); - $this->assertStringContainsString('protected $table = \'utilisateur\';', $this->getFileContent($file)); + $this->assertStringContainsString('protected $table = \'utilisateur\';', $this->getFileContent($file)); } public function testGenerateModelWithOptionDBGroup() @@ -76,43 +79,48 @@ public function testGenerateModelWithOptionDBGroup() $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer); $file = APPPATH . 'Models/User.php'; $this->assertFileExists($file); - $this->assertStringContainsString('protected $DBGroup = \'testing\';', $this->getFileContent($file)); + $this->assertStringContainsString('protected $DBGroup = \'testing\';', $this->getFileContent($file)); } public function testGenerateModelWithOptionReturnArray() { - command('make:model user -return array'); + command('make:model user --return array'); $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer); $file = APPPATH . 'Models/User.php'; $this->assertFileExists($file); - $this->assertStringContainsString('protected $returnType = \'array\';', $this->getFileContent($file)); + $this->assertStringContainsString('protected $returnType = \'array\';', $this->getFileContent($file)); } public function testGenerateModelWithOptionReturnObject() { - command('make:model user -return object'); + command('make:model user --return object'); $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer); $file = APPPATH . 'Models/User.php'; $this->assertFileExists($file); - $this->assertStringContainsString('protected $returnType = \'object\';', $this->getFileContent($file)); + $this->assertStringContainsString('protected $returnType = \'object\';', $this->getFileContent($file)); } public function testGenerateModelWithOptionReturnEntity() { - command('make:model user -return entity'); + command('make:model user --return entity'); $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer); + $file = APPPATH . 'Models/User.php'; $this->assertFileExists($file); - $this->assertStringContainsString('protected $returnType = \'App\Entities\User\';', $this->getFileContent($file)); + $this->assertStringContainsString('protected $returnType = \App\Entities\User::class;', $this->getFileContent($file)); + if (is_file($file)) { unlink($file); } + $file = APPPATH . 'Entities/User.php'; $this->assertFileExists($file); $dir = dirname($file); + if (is_file($file)) { unlink($file); } + if (is_dir($dir)) { rmdir($dir); } @@ -120,13 +128,32 @@ public function testGenerateModelWithOptionReturnEntity() public function testGenerateModelWithOptionSuffix() { - command('make:model user -suffix -return entity'); + command('make:model user --suffix --return entity'); $model = APPPATH . 'Models/UserModel.php'; $entity = APPPATH . 'Entities/UserEntity.php'; $this->assertFileExists($model); $this->assertFileExists($entity); + + unlink($model); + unlink($entity); + rmdir(dirname($entity)); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/5050 + */ + public function testGenerateModelWithSuffixAndMixedPascalCasedName() + { + command('make:model MyTable --suffix --return entity'); + + $model = APPPATH . 'Models/MyTableModel.php'; + $entity = APPPATH . 'Entities/MyTableEntity.php'; + + $this->assertFileExists($model); + $this->assertFileExists($entity); + unlink($model); unlink($entity); rmdir(dirname($entity)); From f4bb2657125e5d10ec5c2b43e55a764f5da4b02f Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 6 Sep 2021 11:08:46 +0900 Subject: [PATCH 004/490] docs: fix indentation of the list --- user_guide_src/source/changelogs/v4.1.4.rst | 68 ++++++++++----------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.1.4.rst b/user_guide_src/source/changelogs/v4.1.4.rst index daca6b616294..79b9b881fd82 100644 --- a/user_guide_src/source/changelogs/v4.1.4.rst +++ b/user_guide_src/source/changelogs/v4.1.4.rst @@ -9,43 +9,43 @@ Breaking Changes: - The following methods were changed from "public" to "protected" to match their parent class methods and better align with their uses: -* ``CodeIgniter\Database\MySQLi\Connection::execute()`` -* ``CodeIgniter\Database\MySQLi\Connection::_fieldData()`` -* ``CodeIgniter\Database\MySQLi\Connection::_indexData()`` -* ``CodeIgniter\Database\MySQLi\Connection::_foreignKeyData()`` -* ``CodeIgniter\Database\Postgre\Builder::_like_statement()`` -* ``CodeIgniter\Database\Postgre\Connection::execute()`` -* ``CodeIgniter\Database\Postgre\Connection::_fieldData()`` -* ``CodeIgniter\Database\Postgre\Connection::_indexData()`` -* ``CodeIgniter\Database\Postgre\Connection::_foreignKeyData()`` -* ``CodeIgniter\Database\SQLSRV\Connection::execute()`` -* ``CodeIgniter\Database\SQLSRV\Connection::_fieldData()`` -* ``CodeIgniter\Database\SQLSRV\Connection::_indexData()`` -* ``CodeIgniter\Database\SQLSRV\Connection::_foreignKeyData()`` -* ``CodeIgniter\Database\SQLite3\Connection::execute()`` -* ``CodeIgniter\Database\SQLite3\Connection::_fieldData()`` -* ``CodeIgniter\Database\SQLite3\Connection::_indexData()`` -* ``CodeIgniter\Database\SQLite3\Connection::_foreignKeyData()`` -* ``CodeIgniter\Images\Handlers\GDHandler::_flatten()`` -* ``CodeIgniter\Images\Handlers\GDHandler::_flip()`` -* ``CodeIgniter\Images\Handlers\ImageMagickHandler::_flatten()`` -* ``CodeIgniter\Images\Handlers\ImageMagickHandler::_flip()`` -* ``CodeIgniter\Test\Mock\MockIncomingRequest::detectURI()`` -* ``CodeIgniter\Test\Mock\MockSecurity.php::sendCookie()`` + * ``CodeIgniter\Database\MySQLi\Connection::execute()`` + * ``CodeIgniter\Database\MySQLi\Connection::_fieldData()`` + * ``CodeIgniter\Database\MySQLi\Connection::_indexData()`` + * ``CodeIgniter\Database\MySQLi\Connection::_foreignKeyData()`` + * ``CodeIgniter\Database\Postgre\Builder::_like_statement()`` + * ``CodeIgniter\Database\Postgre\Connection::execute()`` + * ``CodeIgniter\Database\Postgre\Connection::_fieldData()`` + * ``CodeIgniter\Database\Postgre\Connection::_indexData()`` + * ``CodeIgniter\Database\Postgre\Connection::_foreignKeyData()`` + * ``CodeIgniter\Database\SQLSRV\Connection::execute()`` + * ``CodeIgniter\Database\SQLSRV\Connection::_fieldData()`` + * ``CodeIgniter\Database\SQLSRV\Connection::_indexData()`` + * ``CodeIgniter\Database\SQLSRV\Connection::_foreignKeyData()`` + * ``CodeIgniter\Database\SQLite3\Connection::execute()`` + * ``CodeIgniter\Database\SQLite3\Connection::_fieldData()`` + * ``CodeIgniter\Database\SQLite3\Connection::_indexData()`` + * ``CodeIgniter\Database\SQLite3\Connection::_foreignKeyData()`` + * ``CodeIgniter\Images\Handlers\GDHandler::_flatten()`` + * ``CodeIgniter\Images\Handlers\GDHandler::_flip()`` + * ``CodeIgniter\Images\Handlers\ImageMagickHandler::_flatten()`` + * ``CodeIgniter\Images\Handlers\ImageMagickHandler::_flip()`` + * ``CodeIgniter\Test\Mock\MockIncomingRequest::detectURI()`` + * ``CodeIgniter\Test\Mock\MockSecurity.php::sendCookie()`` - To be compatible with the strict inheritance checks of PHP 8.1, the following method signatures were added return types to match their parents' signatures whenever possible: -* ``CodeIgniter\Cookie\Cookie::offsetExists()`` -* ``CodeIgniter\Cookie\Cookie::offsetSet()`` -* ``CodeIgniter\Cookie\Cookie::offsetUnset()`` -* ``CodeIgniter\Cookie\CookieStore::getIterator()`` -* ``CodeIgniter\I18n\Time::__wakeup()`` -* ``CodeIgniter\Test\Filters\CITestStreamFilter::filter()`` + * ``CodeIgniter\Cookie\Cookie::offsetExists()`` + * ``CodeIgniter\Cookie\Cookie::offsetSet()`` + * ``CodeIgniter\Cookie\Cookie::offsetUnset()`` + * ``CodeIgniter\Cookie\CookieStore::getIterator()`` + * ``CodeIgniter\I18n\Time::__wakeup()`` + * ``CodeIgniter\Test\Filters\CITestStreamFilter::filter()`` - Related to the strict inheritance checks of PHP 8.1, the following session handlers implementing ``SessionHandlerInterface`` have their public methods modified to match the interface: -* ``CodeIgniter\Session\Handlers\ArrayHandler`` -* ``CodeIgniter\Session\Handlers\DatabaseHandler`` -* ``CodeIgniter\Session\Handlers\FileHandler`` -* ``CodeIgniter\Session\Handlers\MemcachedHandler`` -* ``CodeIgniter\Session\Handlers\RedisHandler`` + * ``CodeIgniter\Session\Handlers\ArrayHandler`` + * ``CodeIgniter\Session\Handlers\DatabaseHandler`` + * ``CodeIgniter\Session\Handlers\FileHandler`` + * ``CodeIgniter\Session\Handlers\MemcachedHandler`` + * ``CodeIgniter\Session\Handlers\RedisHandler`` From f1cdeff536a12d903f38a5f39ad84fa9dd55f5ae Mon Sep 17 00:00:00 2001 From: MGatner Date: Tue, 7 Sep 2021 01:51:29 +0000 Subject: [PATCH 005/490] Replace pasted content --- .../source/installation/upgrade_414.rst | 58 +------------------ 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_414.rst b/user_guide_src/source/installation/upgrade_414.rst index 8f84809fb664..613823132351 100644 --- a/user_guide_src/source/installation/upgrade_414.rst +++ b/user_guide_src/source/installation/upgrade_414.rst @@ -38,58 +38,6 @@ If you relied on any of these methods being public (highly unlikely) adjust your Project Files ============= -Numerous files in the project space (root, app, public, writable) received updates. Due to -these files being outside of the system scope they will not be changed without your intervention. -There are some third-party CodeIgniter modules available to assist with merging changes to -the project space: `Explore on Packagist `_. - -.. note:: Except in very rare cases for bug fixes, no changes made to files for the project space - will break your application. All changes noted here are optional until the next major version, - and any mandatory changes will be covered in the sections above. - -Content Changes ---------------- - -The following files received significant changes (including deprecations or visual adjustments) -and it is recommended that you merge the updated versions with your application: - -* ``app/Config/App.php`` -* ``app/Config/Autoload.php`` -* ``app/Config/Cookie.php`` -* ``app/Config/Events.php`` -* ``app/Config/Exceptions.php`` -* ``app/Config/Security.php`` -* ``app/Views/errors/html/*`` -* ``env`` -* ``spark`` - -All Changes ------------ - -This is a list of all files in the project space that received changes; -many will be simple comments or formatting that have no affect on the runtime: - -* ``app/Config/App.php`` -* ``app/Config/Autoload.php`` -* ``app/Config/ContentSecurityPolicy.php`` -* ``app/Config/Cookie.php`` -* ``app/Config/Events.php`` -* ``app/Config/Exceptions.php`` -* ``app/Config/Logger.php`` -* ``app/Config/Mimes.php`` -* ``app/Config/Modules.php`` -* ``app/Config/Security.php`` -* ``app/Controllers/BaseController.php`` -* ``app/Views/errors/html/debug.css`` -* ``app/Views/errors/html/error_404.php`` -* ``app/Views/errors/html/error_exception.php`` -* ``app/Views/welcome_message.php`` -* ``composer.json`` -* ``contributing/guidelines.rst`` -* ``env`` -* ``phpstan.neon.dist`` -* ``phpunit.xml.dist`` -* ``public/.htaccess`` -* ``public/index.php`` -* ``rector.php`` -* ``spark`` +All files in the project space were reformatted with the new coding style. This will not affect +existing code but you may want to apply the updated coding style to your own projects to keep +them in line with the framework's version of these files. From 09609d5ab050e89d26dac6e93a465a27aff66254 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 13 Jun 2021 23:37:10 -0500 Subject: [PATCH 006/490] New mock() test helper and expanded MockCache with assertions. --- system/Cache/CacheFactory.php | 18 ++++- system/Exceptions/TestException.php | 25 ++++++ system/Helpers/test_helper.php | 32 +++++++- system/Language/en/Test.php | 15 ++++ system/Test/Mock/MockCache.php | 98 +++++++++++++++++------ tests/system/Cache/CacheMockTest.php | 46 +++++++++++ user_guide_src/source/testing/index.rst | 3 +- user_guide_src/source/testing/mocking.rst | 53 ++++++++++++ 8 files changed, 260 insertions(+), 30 deletions(-) create mode 100644 system/Exceptions/TestException.php create mode 100644 system/Language/en/Test.php create mode 100644 tests/system/Cache/CacheMockTest.php create mode 100644 user_guide_src/source/testing/mocking.rst diff --git a/system/Cache/CacheFactory.php b/system/Cache/CacheFactory.php index 720fae0e4c6c..875dcd3f7bcd 100644 --- a/system/Cache/CacheFactory.php +++ b/system/Cache/CacheFactory.php @@ -13,6 +13,7 @@ use CodeIgniter\Cache\Exceptions\CacheException; use CodeIgniter\Exceptions\CriticalError; +use CodeIgniter\Test\Mock\MockCache; use Config\Cache; /** @@ -20,6 +21,20 @@ */ class CacheFactory { + /** + * The class to use when mocking + * + * @var string + */ + public static $mockClass = MockCache::class; + + /** + * The service to inject the mock as + * + * @var string + */ + public static $mockServiceName = 'cache'; + /** * Attempts to create the desired cache handler, based upon the * @@ -42,14 +57,12 @@ public static function getHandler(Cache $config, ?string $handler = null, ?strin throw CacheException::forHandlerNotFound(); } - // Get an instance of our handler. $adapter = new $config->validHandlers[$handler]($config); if (! $adapter->isSupported()) { $adapter = new $config->validHandlers[$backup]($config); if (! $adapter->isSupported()) { - // Log stuff here, don't throw exception. No need to raise a fuss. // Fall back to the dummy adapter. $adapter = new $config->validHandlers['dummy'](); } @@ -60,7 +73,6 @@ public static function getHandler(Cache $config, ?string $handler = null, ?strin try { $adapter->initialize(); } catch (CriticalError $e) { - // log the fact that an exception occurred as well what handler we are resorting to log_message('critical', $e->getMessage() . ' Resorting to using ' . $backup . ' handler.'); // get the next best cache handler (or dummy if the $backup also fails) diff --git a/system/Exceptions/TestException.php b/system/Exceptions/TestException.php new file mode 100644 index 000000000000..a1c4c51cf814 --- /dev/null +++ b/system/Exceptions/TestException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception for automatic logging. + */ +class TestException extends CriticalError +{ + use DebugTraceableTrait; + + public static function forInvalidMockClass(string $name) + { + return new static(lang('Test.invalidMockClass', [$name])); + } +} diff --git a/system/Helpers/test_helper.php b/system/Helpers/test_helper.php index e20054ab6893..a73446a0ac5f 100644 --- a/system/Helpers/test_helper.php +++ b/system/Helpers/test_helper.php @@ -9,8 +9,10 @@ * the LICENSE file that was distributed with this source code. */ +use CodeIgniter\Exceptions\TestException; use CodeIgniter\Model; use CodeIgniter\Test\Fabricator; +use Config\Services; // CodeIgniter Test Helpers @@ -20,16 +22,14 @@ * * @param Model|object|string $model Instance or name of the model * @param array|null $overrides Overriding data to pass to Fabricator::setOverrides() - * @param mixed $persist + * @param bool $persist * * @return array|object */ function fake($model, ?array $overrides = null, $persist = true) { - // Get a model-appropriate Fabricator instance $fabricator = new Fabricator($model); - // Set overriding data, if necessary if ($overrides) { $fabricator->setOverrides($overrides); } @@ -41,3 +41,29 @@ function fake($model, ?array $overrides = null, $persist = true) return $fabricator->make(); } } + +if (! function_exists('mock')) { + /** + * Used within our test suite to mock certain system tools. + * All tools using this MUST use the MockableTrait + * + * @param string $className Fully qualified class name + */ + function mock(string $className) + { + $mockClass = $className::$mockClass; + $mockService = $className::$mockServiceName; + + if (empty($mockClass) || ! class_exists($mockClass)) { + throw TestException::forInvalidMockClass($mockClass); + } + + $mock = new $mockClass(); + + if (! empty($mockService)) { + Services::injectMock($mockService, $mock); + } + + return $mock; + } +} diff --git a/system/Language/en/Test.php b/system/Language/en/Test.php new file mode 100644 index 000000000000..a3f5d7ef6f02 --- /dev/null +++ b/system/Language/en/Test.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +// Testing language settings +return [ + 'invalidMockClass' => '{0} is not a valid Mock class', +]; diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index e6bc200fdd1f..a2b1c12cd68f 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -14,6 +14,7 @@ use Closure; use CodeIgniter\Cache\CacheInterface; use CodeIgniter\Cache\Handlers\BaseHandler; +use PHPUnit\Framework\Assert; class MockCache extends BaseHandler implements CacheInterface { @@ -31,6 +32,13 @@ class MockCache extends BaseHandler implements CacheInterface */ protected $expirations = []; + /** + * If true, will not cache any data. + * + * @var bool + */ + protected $bypass = false; + /** * Takes care of any handler-specific setup that must be done. */ @@ -49,16 +57,12 @@ public function get(string $key) { $key = static::validateKey($key, $this->prefix); - return $this->cache[$key] ?? null; + return array_key_exists($key, $this->cache) ? $this->cache[$key] : null; } /** * Get an item from the cache, or execute the given Closure and store the result. * - * @param string $key Cache item name - * @param int $ttl Time to live - * @param Closure $callback Callback return value - * * @return mixed */ public function remember(string $key, int $ttl, Closure $callback) @@ -89,6 +93,10 @@ public function remember(string $key, int $ttl, Closure $callback) */ public function save(string $key, $value, int $ttl = 60, bool $raw = false) { + if ($this->bypass) { + return false; + } + $key = static::validateKey($key, $this->prefix); $this->cache[$key] = $value; @@ -100,8 +108,6 @@ public function save(string $key, $value, int $ttl = 60, bool $raw = false) /** * Deletes a specific item from the cache store. * - * @param string $key Cache item name - * * @return bool */ public function delete(string $key) @@ -120,8 +126,6 @@ public function delete(string $key) /** * Deletes items from the cache store matching a given pattern. * - * @param string $pattern Cache items glob-style pattern - * * @return int */ public function deleteMatching(string $pattern) @@ -141,9 +145,6 @@ public function deleteMatching(string $pattern) /** * Performs atomic incrementation of a raw stored value. * - * @param string $key Cache ID - * @param int $offset Step/value to increase by - * * @return bool */ public function increment(string $key, int $offset = 1) @@ -163,9 +164,6 @@ public function increment(string $key, int $offset = 1) /** * Performs atomic decrementation of a raw stored value. * - * @param string $key Cache ID - * @param int $offset Step/value to increase by - * * @return bool */ public function decrement(string $key, int $offset = 1) @@ -212,10 +210,7 @@ public function getCacheInfo() /** * Returns detailed information about the specific item in the cache. * - * @param string $key Cache item name. - * - * @return array|null - * Returns null if the item does not exist, otherwise array + * @return array|null Returns null if the item does not exist, otherwise array * with at least the 'expire' key for absolute epoch expiry (or null). */ public function getMetaData(string $key) @@ -230,16 +225,73 @@ public function getMetaData(string $key) return null; } - return [ - 'expire' => $this->expirations[$key], - ]; + return ['expire' => $this->expirations[$key]]; } /** - * Determines if the driver is supported on this system. + * Determine if the driver is supported on this system. */ public function isSupported(): bool { return true; } + + //-------------------------------------------------------------------- + // Test Helpers + //-------------------------------------------------------------------- + + /** + * Instructs the class to ignore all + * requests to cache an item, and always "miss" + * when checked for existing data. + * + * @return $this + */ + public function bypass(bool $bypass = true) + { + $this->clean(); + $this->bypass = $bypass; + + return $this; + } + + //-------------------------------------------------------------------- + // Additional Assertions + //-------------------------------------------------------------------- + + /** + * Asserts that the cache has an item named $key. + * The value is not checked since storing false or null + * values is valid. + */ + public function assertHas(string $key) + { + Assert::assertNotNull($this->get($key), "The cache does not have an item named: `{$key}`"); + } + + /** + * Asserts that the cache has an item named $key with a value matching $value. + * + * @param mixed $value + */ + public function assertHasValue(string $key, $value = null) + { + $item = $this->get($key); + + // Let assertHas handle throwing the error for consistency + // if the key is not found + if (empty($item)) { + $this->assertHas($key); + } + + Assert::assertEquals($value, $this->get($key), "The cached item `{$key}` does not equal match expectation. Found: " . print_r($value, true)); + } + + /** + * Asserts that the cache does NOT have an item named $key. + */ + public function assertMissing(string $key) + { + Assert::assertFalse(array_key_exists($key, $this->cache), "The cached item named `{$key}` exists."); + } } diff --git a/tests/system/Cache/CacheMockTest.php b/tests/system/Cache/CacheMockTest.php new file mode 100644 index 000000000000..860623478693 --- /dev/null +++ b/tests/system/Cache/CacheMockTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Cache; + +use CodeIgniter\Cache\Handlers\BaseHandler; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockCache; + +/** + * @internal + */ +final class CacheMockTest extends CIUnitTestCase +{ + public function testMockReturnsMockCacheClass() + { + $this->assertInstanceOf(BaseHandler::class, service('cache')); + + $mock = mock(CacheFactory::class); + $this->assertInstanceOf(MockCache::class, $mock); + $this->assertInstanceOf(MockCache::class, service('cache')); + } + + public function testMockCaching() + { + $mock = mock(CacheFactory::class); + + // Ensure it stores the value normally + $mock->save('foo', 'bar'); + $mock->assertHas('foo'); + $mock->assertHasValue('foo', 'bar'); + + // Try it again with bypass on + $mock->bypass(); + $mock->save('foo', 'bar'); + $mock->assertMissing('foo'); + } +} diff --git a/user_guide_src/source/testing/index.rst b/user_guide_src/source/testing/index.rst index 878cc9918355..949a68b99b51 100644 --- a/user_guide_src/source/testing/index.rst +++ b/user_guide_src/source/testing/index.rst @@ -2,7 +2,7 @@ Testing ####### -CodeIgniter ships with a number of tools to help you test and debug your application thoroughly. +CodeIgniter ships with a number of tools to help you test and debug your application thoroughly. The following sections should get you quickly testing your applications. .. toctree:: @@ -16,3 +16,4 @@ The following sections should get you quickly testing your applications. response benchmark debugging + Mocking diff --git a/user_guide_src/source/testing/mocking.rst b/user_guide_src/source/testing/mocking.rst new file mode 100644 index 000000000000..ea0a30922dd4 --- /dev/null +++ b/user_guide_src/source/testing/mocking.rst @@ -0,0 +1,53 @@ +###################### +Mocking System Classes +###################### + +Several classes within the framework provide mocked versions of the classes that can be used during testing. These classes +can take the place of the normal class during test execution, often providing additional assertions to test that actions +have taken place (or not taken place) during the execution of the test. This might be checking data gets cached correctly, +emails were sent correctly, etc. + +.. contents:: + :local: + :depth: 1 + +Cache +===== + +You can mock the cache with the ``mock()`` method, using the ``CacheFactory`` as its only parameter. +:: + + $mock = mock(CodeIgniter\Cache\CacheFactory::class); + +While this returns an instance of ``CodeIgniter\Test\Mock\MockCache`` that you can use directly, it also inserts the +mock into the Service class, so any calls within your code to ``service('cache')`` or ``Config\Services::cache()`` will +use the mocked class within its place. + +When using this in more than one test method within a single file you should call either the ``clean()`` or ``bypass()`` +methods during the test ``setUp()`` to ensure a clean slate when your tests run. + +Additional Methods +------------------ + +You can instruct the mocked cache handler to never do any caching with the ``bypass()`` method. This will emulate +using the dummy handler and ensures that your test does not rely on cached data for your tests. +:: + + $mock = mock(CodeIgniter\Cache\CacheFactory::class); + // Never cache any items during this test. + $mock->bypass(); + +Available Assertions +-------------------- + +The following new assertions are available on the mocked class for using during testing: +:: + + $mock = mock(CodeIgniter\Cache\CacheFactory::class); + + // Assert that a cached item named $key exists + $mock->assertHas($key); + // Assert that a cached item named $key exists with a value of $value + $mock->assertHasValue($key, $value); + // Assert that a cached item named $key does NOT exist + $mock->assertMissing($key); From 8710e3ed949661e96fe089a94c1776317ab6bb21 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Wed, 16 Jun 2021 22:30:50 -0500 Subject: [PATCH 007/490] Updated assert to assertArrayNotHasKey per suggestion. --- system/Test/Mock/MockCache.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index a2b1c12cd68f..ebdacccbbef5 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -284,7 +284,7 @@ public function assertHasValue(string $key, $value = null) $this->assertHas($key); } - Assert::assertEquals($value, $this->get($key), "The cached item `{$key}` does not equal match expectation. Found: " . print_r($value, true)); + Assert::assertSame($value, $this->get($key), "The cached item `{$key}` does not equal match expectation. Found: " . print_r($value, true)); } /** @@ -292,6 +292,6 @@ public function assertHasValue(string $key, $value = null) */ public function assertMissing(string $key) { - Assert::assertFalse(array_key_exists($key, $this->cache), "The cached item named `{$key}` exists."); + Assert::assertArrayNotHasKey($key, $this->cache, "The cached item named `{$key}` exists."); } } From d2e9a64b00d0bdb83fd8cac259a3f374d606b896 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 14 Jun 2021 08:13:52 -0500 Subject: [PATCH 008/490] Update system/Helpers/test_helper.php Co-authored-by: MGatner --- system/Helpers/test_helper.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/system/Helpers/test_helper.php b/system/Helpers/test_helper.php index a73446a0ac5f..fe6b06794ccd 100644 --- a/system/Helpers/test_helper.php +++ b/system/Helpers/test_helper.php @@ -45,14 +45,13 @@ function fake($model, ?array $overrides = null, $persist = true) if (! function_exists('mock')) { /** * Used within our test suite to mock certain system tools. - * All tools using this MUST use the MockableTrait * * @param string $className Fully qualified class name */ function mock(string $className) { $mockClass = $className::$mockClass; - $mockService = $className::$mockServiceName; + $mockService = $className::$mockServiceName ?? ''; if (empty($mockClass) || ! class_exists($mockClass)) { throw TestException::forInvalidMockClass($mockClass); From 0a2b74b2ecaa3802f3009a0df826499eaefce76a Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 14 Jun 2021 08:17:05 -0500 Subject: [PATCH 009/490] Update user_guide_src/source/testing/mocking.rst Co-authored-by: MGatner --- user_guide_src/source/testing/mocking.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/testing/mocking.rst b/user_guide_src/source/testing/mocking.rst index ea0a30922dd4..03b464cf6570 100644 --- a/user_guide_src/source/testing/mocking.rst +++ b/user_guide_src/source/testing/mocking.rst @@ -2,7 +2,7 @@ Mocking System Classes ###################### -Several classes within the framework provide mocked versions of the classes that can be used during testing. These classes +Several components within the framework provide mocked versions of their classes that can be used during testing. These classes can take the place of the normal class during test execution, often providing additional assertions to test that actions have taken place (or not taken place) during the execution of the test. This might be checking data gets cached correctly, emails were sent correctly, etc. From 944152fe130554a1529d8070bb2ec285975519aa Mon Sep 17 00:00:00 2001 From: MGatner Date: Sun, 23 May 2021 19:09:49 +0000 Subject: [PATCH 010/490] Add Publisher --- system/Language/en/Publisher.php | 17 + .../Exceptions/PublisherException.php | 32 + system/Publisher/Publisher.php | 713 ++++++++++++++++++ tests/_support/Publishers/TestPublisher.php | 16 + 4 files changed, 778 insertions(+) create mode 100644 system/Language/en/Publisher.php create mode 100644 system/Publisher/Exceptions/PublisherException.php create mode 100644 system/Publisher/Publisher.php create mode 100644 tests/_support/Publishers/TestPublisher.php diff --git a/system/Language/en/Publisher.php b/system/Language/en/Publisher.php new file mode 100644 index 000000000000..ada599074efe --- /dev/null +++ b/system/Language/en/Publisher.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// Publisher language settings +return [ + 'expectedFile' => 'Publisher::{0} expects a valid file.', + 'expectedDirectory' => 'Publisher::{0} expects a valid directory.', + 'collision' => 'Publisher encountered an unexpected {0} while copying {1} to {2}.', +]; diff --git a/system/Publisher/Exceptions/PublisherException.php b/system/Publisher/Exceptions/PublisherException.php new file mode 100644 index 000000000000..23a4b54d3e57 --- /dev/null +++ b/system/Publisher/Exceptions/PublisherException.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeIgniter\Publisher\Exceptions; + +use CodeIgniter\Exceptions\FrameworkException; + +class PublisherException extends FrameworkException +{ + public static function forExpectedDirectory(string $caller) + { + return new static(lang('Publisher.expectedDirectory', [$caller])); + } + + public static function forExpectedFile(string $caller) + { + return new static(lang('Publisher.expectedFile', [$caller])); + } + + public static function forCollision(string $from, string $to) + { + return new static(lang('Publisher.collision', [filetype($to), $from, $to])); + } +} diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php new file mode 100644 index 000000000000..8f599d3292b6 --- /dev/null +++ b/system/Publisher/Publisher.php @@ -0,0 +1,713 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeIgniter\Publisher; + +use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Files\File; +use CodeIgniter\HTTP\URI; +use CodeIgniter\Publisher\Exceptions\PublisherException; +use Throwable; + +/** + * Publisher Class + * + * Publishers read in file paths from a variety + * of sources and copy the files out to different + * destinations. + * This class acts both as a base for individual + * publication directives as well as the mode of + * discovery for said instances. + * In this class a "file" is a full path to a + * verified file while a "path" is relative to + * to its source or destination and may indicate + * either a file or directory fo unconfirmed + * existence. + * Class failures throw a PublisherException, + * but some underlying methods may percolate + * different exceptions, like FileException, + * FileNotFoundException, InvalidArgumentException. + * Write operations will catch all errors in the + * file-specific $errors property to minimize + * impact of partial batch operations. + */ +class Publisher +{ + /** + * Array of discovered Publishers. + * + * @var array + */ + private static $discovered = []; + + /** + * Directory to use for methods + * that need temporary storage. + * Created on-the-fly as needed. + * + * @var string|null + */ + private $scratch; + + /** + * The current list of files. + * + * @var string[] + */ + private $files = []; + + /** + * Exceptions for specific files + * from the last write operation. + * + * @var array + */ + private $errors = []; + + /** + * Base path to use for the source. + * + * @var string + */ + protected $source = ROOTPATH; + + /** + * Base path to use for the destination. + * + * @var string + */ + protected $destination = FCPATH; + + /** + * Discovers and returns all Publishers + * in the specified namespace directory. + * + * @return self[] + */ + public static function discover($directory = 'Publishers'): array + { + if (isset(self::$discovered[$directory])) + { + return self::$discovered[$directory]; + } + self::$discovered[$directory] = []; + + /** @var FileLocator $locator */ + $locator = service('locator'); + + if ([] === $files = $locator->listFiles($directory)) + { + return []; + } + + // Loop over each file checking to see if it is a Primer + foreach ($files as $file) + { + $className = $locator->findQualifiedNameFromPath($file); + + if (is_string($className) && class_exists($className) && is_a($className, self::class, true)) + { + self::$discovered[$directory][] = new $className(); + } + } + sort(self::$discovered[$directory]); + + return self::$discovered[$directory]; + } + + //-------------------------------------------------------------------- + + /** + * Resolves a full path and verifies + * it is an actual directory. + * + * @param string $directory + * + * @return string + */ + private static function resolveDirectory(string $directory): string + { + if (! is_dir($directory = set_realpath($directory))) + { + $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + throw PublisherException::forExpectedDirectory($caller['function']); + } + + return $directory; + } + + /** + * Resolves a full path and verifies + * it is an actual file. + * + * @param string $file + * + * @return string + */ + private static function resolveFile(string $file): string + { + if (! is_file($file = set_realpath($file))) + { + $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + throw PublisherException::forExpectedFile($caller['function']); + } + + return $file; + } + + //-------------------------------------------------------------------- + + /** + * Filters an array of files, removing files not + * part of the given directory (recursive). + * + * @param string[] $files + * @param string $directory + * + * @return string[] + */ + private static function filterFiles(array $files, string $directory): array + { + $directory = self::resolveDirectory($directory); + + return array_filter($files, function ($value) use ($directory) { + return strpos($value, $directory) === 0; + }); + } + + /** + * Returns any files whose basename matches + * the given pattern. + * + * @param string[] $files + * @param string $pattern Regex or pseudo-regex string + * + * @return string[] + */ + private static function matchFiles(array $files, string $pattern) + { + // Convert pseudo-regex into their true form + if (@preg_match($pattern, null) === false) // @phpstan-ignore-line + { + $pattern = str_replace( + ['#', '.', '*', '?'], + ['\#', '\.', '.*', '.'], + $pattern + ); + $pattern = "#{$pattern}#"; + } + + return array_filter($files, function ($value) use ($pattern) { + return (bool) preg_match($pattern, basename($value)); + }); + } + + //-------------------------------------------------------------------- + + /** + * Removes a directory and all its files + * and subdirectories. + * + * @param string $directory + * + * @return void + */ + private static function wipeDirectory(string $directory) + { + if (is_dir($directory)) + { + // Try a few times in case of lingering locks + $attempts = 10; + while ((bool) $attempts && ! delete_files($directory, true, false, true)) + { + $attempts--; + usleep(100000); // .1s + } + + @rmdir($directory); + } + } + + /** + * Copies a file with directory creation + * and identical file awareness. + * Intentionally allows errors. + * + * @param string $from + * @param string $to + * @param bool $replace + * + * @return void + * + * @throws PublisherException For unresolvable collisions + */ + private static function safeCopyFile(string $from, string $to, bool $replace): void + { + // Check for an existing file + if (file_exists($to)) + { + // If not replacing or if files are identical then consider successful + if (! $replace || same_file($from, $to)) + { + return; + } + + // If it is a directory then do not try to remove it + if (is_dir($to)) + { + throw PublisherException::forCollision($from, $to); + } + + // Try to remove anything else + unlink($to); + } + + // Allow copy() to throw errors + copy($from, $to); + } + + //-------------------------------------------------------------------- + + /** + * Loads the helper and verifies the + * source and destination directories. + * + * @param string|null $source + * @param string|null $destination + */ + public function __construct(string $source = null, string $destination = null) + { + helper(['filesystem']); + + $this->source = self::resolveDirectory($source ?? $this->source); + $this->destination = self::resolveDirectory($destination ?? $this->destination); + } + + /** + * Cleans up any temporary files + * in the scratch space. + */ + public function __destruct() + { + if (isset($this->scratch)) + { + self::wipeDirectory($this->scratch); + + $this->scratch = null; + } + } + + /** + * Reads in file sources and copies out + * the files to their destinations. + * This method should be reimplemented by + * child classes intended for discovery. + * + * @return void + */ + public function publish() + { + } + + //-------------------------------------------------------------------- + + /** + * Returns the temporary workspace, + * creating it if necessary. + * + * @return string + */ + protected function getScratch(): string + { + if (is_null($this->scratch)) + { + $this->scratch = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)) . DIRECTORY_SEPARATOR; + mkdir($this->scratch, 0700); + } + + return $this->scratch; + } + + /** + * Returns any errors from the last + * write operation. + * + * @return array + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * Optimizes and returns the + * current file list. + * + * @return string[] + */ + public function getFiles(): array + { + $this->files = array_unique($this->files, SORT_STRING); + sort($this->files, SORT_STRING); + + return $this->files; + } + + /** + * Sets the file list directly. + * Files are still subject to verification. + * This works as a "reset" method with []. + * + * @param string[] $files A new file list to use + * + * @return $this + */ + public function setFiles(array $files) + { + $this->files = []; + + return $this->addFiles($files); + } + + //-------------------------------------------------------------------- + + /** + * Verifies and adds files to the list. + * + * @param string[] $files + * + * @return $this + */ + public function addFiles(array $files) + { + foreach ($files as $file) + { + $this->addFile($file); + } + + return $this; + } + + /** + * Verifies and adds a single file + * to the file list. + * + * @param string $file + * + * @return $this + */ + public function addFile(string $file) + { + $this->files[] = self::resolveFile($file); + + return $this; + } + + /** + * Removes files from the list. + * + * @param string[] $files + * + * @return $this + */ + public function removeFiles(array $files) + { + $this->files = array_diff($this->files, $files); + + return $this; + } + + /** + * Removes a single file from the list. + * + * @param string $file + * + * @return $this + */ + public function removeFile(string $file) + { + return $this->removeFiles([$file]); + } + + //-------------------------------------------------------------------- + + /** + * Verifies and adds files from each + * directory to the list. + * + * @param string[] $directories + * @param bool $recursive + * + * @return $this + */ + public function addDirectories(array $directories, bool $recursive = false) + { + foreach ($directories as $directory) + { + $this->addDirectory($directory, $recursive); + } + + return $this; + } + + /** + * Verifies and adds all files + * from a directory. + * + * @param string $directory + * @param bool $recursive + * + * @return $this + */ + public function addDirectory(string $directory, bool $recursive = false) + { + $directory = self::resolveDirectory($directory); + + // Map the directory to depth 2 to so directories become arrays + foreach (directory_map($directory, 2, true) as $key => $path) + { + if (is_string($path)) + { + $this->addFile($directory . $path); + } + elseif ($recursive && is_array($path)) + { + $this->addDirectory($directory . $key, true); + } + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Verifies and adds paths to the list. + * + * @param string[] $paths + * @param bool $recursive + * + * @return $this + */ + public function addPaths(array $paths, bool $recursive = true) + { + foreach ($paths as $path) + { + $this->addPath($path, $recursive); + } + + return $this; + } + + /** + * Adds a single path to the file list. + * + * @param string $path + * @param bool $recursive + * + * @return $this + */ + public function addPath(string $path, bool $recursive = true) + { + $full = $this->source . $path; + + // Test for a directory + try + { + $directory = self::resolveDirectory($full); + } + catch (PublisherException $e) + { + return $this->addFile($full); + } + + return $this->addDirectory($full, $recursive); + } + + //-------------------------------------------------------------------- + + /** + * Downloads and stages files from + * an array of URIs. + * + * @param string[] $uris + * + * @return $this + */ + public function addUris(array $uris) + { + foreach ($uris as $uri) + { + $this->addUri($uri); + } + + return $this; + } + + /** + * Downloads a file from the URI + * and adds it to the file list. + * + * @param string $uri Because HTTP\URI is stringable it will still be accepted + * + * @return $this + */ + public function addUri(string $uri) + { + // Figure out a good filename (using URI strips queries and fragments) + $file = $this->getScratch() . basename((new URI($uri))->getPath()); + + // Get the content and write it to the scratch space + $response = service('curlrequest')->get($uri); + write_file($file, $response->getBody()); + + return $this->addFile($file); + } + + //-------------------------------------------------------------------- + + /** + * Removes any files from the list that match + * the supplied pattern (within the optional scope). + * + * @param string $pattern Regex or pseudo-regex string + * @param string|null $scope A directory to limit the scope + * + * @return $this + */ + public function removePattern(string $pattern, string $scope = null) + { + if ($pattern === '') + { + return $this; + } + + // Start with all files or those in scope + $files = is_null($scope) + ? $this->files + : self::filterFiles($this->files, $scope); + + // Remove any files that match the pattern + return $this->removeFiles(self::matchFiles($files, $pattern)); + } + + /** + * Keeps only the files from the list that match + * the supplied pattern (within the optional scope). + * + * @param string $pattern Regex or pseudo-regex string + * @param string|null $scope A directory to limit the scope + * + * @return $this + */ + public function retainPattern(string $pattern, string $scope = null) + { + if ($pattern === '') + { + return $this; + } + + // Start with all files or those in scope + $files = is_null($scope) + ? $this->files + : self::filterFiles($this->files, $scope); + + // Match the pattern within the scoped files + $matched = self::matchFiles($files, $pattern); + + // ... and remove their inverse + return $this->removeFiles(array_diff($files, $matched)); + } + + //-------------------------------------------------------------------- + + /** + * Removes the destination and all its files and folders. + * + * @return $this + */ + public function wipe() + { + self::wipeDirectory($this->destination); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Copies all files into the destination. + * Does not create directory structure. + * + * @param bool $replace Whether to overwrite existing files. + * + * @return boolean Whether all files were copied successfully + */ + public function copy(bool $replace = true): bool + { + $this->errors = []; + + foreach ($this->getFiles() as $file) + { + $to = $this->destination . basename($file); + + try + { + self::safeCopyFile($file, $to, $replace); + } + catch (Throwable $e) + { + $this->errors[$file] = $e; + } + } + + return $this->errors === []; + } + + /** + * Merges all files into the destination. + * Creates a mirrored directory structure + * only for files from source. + * + * @param bool $replace Whether to overwrite existing files. + * + * @return boolean Whether all files were copied successfully + */ + public function merge(bool $replace = true): bool + { + $this->errors = []; + + // Get the file from source for special handling + $sourced = self::matchFiles($this->getFiles(), $this->source); + + // Handle everything else with a flat copy + $this->files = array_diff($this->files, $sourced); + $this->copy($replace); + + // Copy each sourced file to its relative destination + foreach ($sourced as $file) + { + // Resolve the destination path + $to = $this->destination . substr($file, strlen($this->source)); + + try + { + self::safeCopyFile($file, $to, $replace); + } + catch (Throwable $e) + { + $this->errors[$file] = $e; + } + } + + return $this->errors === []; + } +} diff --git a/tests/_support/Publishers/TestPublisher.php b/tests/_support/Publishers/TestPublisher.php new file mode 100644 index 000000000000..8ef30ca90024 --- /dev/null +++ b/tests/_support/Publishers/TestPublisher.php @@ -0,0 +1,16 @@ +downloadFromUrls($urls)->mergeToDirectory(FCPATH . 'assets'); + } +} From b2a16e9d402ab80aaa73150ffff2ee47d5a3e8da Mon Sep 17 00:00:00 2001 From: MGatner Date: Mon, 24 May 2021 01:10:02 +0000 Subject: [PATCH 011/490] Add tests --- system/Publisher/Publisher.php | 18 +- tests/system/Publisher/PublisherInputTest.php | 472 ++++++++++++++++++ .../system/Publisher/PublisherOutputTest.php | 202 ++++++++ .../system/Publisher/PublisherSupportTest.php | 199 ++++++++ 4 files changed, 887 insertions(+), 4 deletions(-) create mode 100644 tests/system/Publisher/PublisherInputTest.php create mode 100644 tests/system/Publisher/PublisherOutputTest.php create mode 100644 tests/system/Publisher/PublisherSupportTest.php diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index 8f599d3292b6..d7f797e00312 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -108,8 +108,8 @@ public static function discover($directory = 'Publishers'): array return []; } - // Loop over each file checking to see if it is a Primer - foreach ($files as $file) + // Loop over each file checking to see if it is a Publisher + foreach (array_unique($files) as $file) { $className = $locator->findQualifiedNameFromPath($file); @@ -228,8 +228,10 @@ private static function wipeDirectory(string $directory) $attempts = 10; while ((bool) $attempts && ! delete_files($directory, true, false, true)) { + // @codeCoverageIgnoreStart $attempts--; usleep(100000); // .1s + // @codeCoverageIgnoreEnd } @rmdir($directory); @@ -270,6 +272,13 @@ private static function safeCopyFile(string $from, string $to, bool $replace): v unlink($to); } + // Make sure the directory exists + $directory = pathinfo($to, PATHINFO_DIRNAME); + if (! is_dir($directory)) + { + mkdir($directory, 0775, true); + } + // Allow copy() to throw errors copy($from, $to); } @@ -311,10 +320,11 @@ public function __destruct() * This method should be reimplemented by * child classes intended for discovery. * - * @return void + * @return bool */ public function publish() { + return $this->addPath('/')->merge(true); } //-------------------------------------------------------------------- @@ -686,7 +696,7 @@ public function merge(bool $replace = true): bool $this->errors = []; // Get the file from source for special handling - $sourced = self::matchFiles($this->getFiles(), $this->source); + $sourced = self::filterFiles($this->getFiles(), $this->source); // Handle everything else with a flat copy $this->files = array_diff($this->files, $sourced); diff --git a/tests/system/Publisher/PublisherInputTest.php b/tests/system/Publisher/PublisherInputTest.php new file mode 100644 index 000000000000..553a92b14c53 --- /dev/null +++ b/tests/system/Publisher/PublisherInputTest.php @@ -0,0 +1,472 @@ +assertSame([], $this->getPrivateProperty($publisher, 'files')); + + $publisher->addFile($this->file); + $this->assertSame([$this->file], $this->getPrivateProperty($publisher, 'files')); + } + + public function testAddFileMissing() + { + $publisher = new Publisher(); + + $this->expectException(PublisherException::class); + $this->expectExceptionMessage(lang('Publisher.expectedFile', ['addFile'])); + + $publisher->addFile('TheHillsAreAlive.bmp'); + } + + public function testAddFileDirectory() + { + $publisher = new Publisher(); + + $this->expectException(PublisherException::class); + $this->expectExceptionMessage(lang('Publisher.expectedFile', ['addFile'])); + + $publisher->addFile($this->directory); + } + + public function testAddFiles() + { + $publisher = new Publisher(); + $files = [ + $this->file, + $this->file, + ]; + + $publisher->addFiles($files); + $this->assertSame($files, $this->getPrivateProperty($publisher, 'files')); + } + + //-------------------------------------------------------------------- + + public function testGetFiles() + { + $publisher = new Publisher(); + $publisher->addFile($this->file); + + $this->assertSame([$this->file], $publisher->getFiles()); + } + + public function testGetFilesSorts() + { + $publisher = new Publisher(); + $files = [ + $this->file, + $this->directory . 'apple.php', + ]; + + $publisher->addFiles($files); + + $this->assertSame(array_reverse($files), $publisher->getFiles()); + } + + public function testGetFilesUniques() + { + $publisher = new Publisher(); + $files = [ + $this->file, + $this->file, + ]; + + $publisher->addFiles($files); + $this->assertSame([$this->file], $publisher->getFiles()); + } + + public function testSetFiles() + { + $publisher = new Publisher(); + + $publisher->setFiles([$this->file]); + $this->assertSame([$this->file], $publisher->getFiles()); + } + + public function testSetFilesInvalid() + { + $publisher = new Publisher(); + + $this->expectException(PublisherException::class); + $this->expectExceptionMessage(lang('Publisher.expectedFile', ['addFile'])); + + $publisher->setFiles(['flerb']); + } + + //-------------------------------------------------------------------- + + public function testRemoveFile() + { + $publisher = new Publisher(); + $files = [ + $this->file, + $this->directory . 'apple.php', + ]; + + $publisher->addFiles($files); + + $publisher->removeFile($this->file); + + $this->assertSame([$this->directory . 'apple.php'], $publisher->getFiles()); + } + + public function testRemoveFiles() + { + $publisher = new Publisher(); + $files = [ + $this->file, + $this->directory . 'apple.php', + ]; + + $publisher->addFiles($files); + + $publisher->removeFiles($files); + + $this->assertSame([], $publisher->getFiles()); + } + + //-------------------------------------------------------------------- + + public function testAddDirectoryInvalid() + { + $publisher = new Publisher(); + + $this->expectException(PublisherException::class); + $this->expectExceptionMessage(lang('Publisher.expectedDirectory', ['addDirectory'])); + + $publisher->addDirectory($this->file); + } + + public function testAddDirectory() + { + $publisher = new Publisher(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + ]; + + $publisher->addDirectory($this->directory); + + $this->assertSame($expected, $publisher->getFiles()); + } + + public function testAddDirectoryRecursive() + { + $publisher = new Publisher(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $publisher->addDirectory(SUPPORTPATH . 'Files', true); + + $this->assertSame($expected, $publisher->getFiles()); + } + + public function testAddDirectories() + { + $publisher = new Publisher(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $publisher->addDirectories([ + $this->directory, + SUPPORTPATH . 'Files/baker', + ]); + + $this->assertSame($expected, $publisher->getFiles()); + } + + public function testAddDirectoriesRecursive() + { + $publisher = new Publisher(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + SUPPORTPATH . 'Log/Handlers/TestHandler.php', + ]; + + $publisher->addDirectories([ + SUPPORTPATH . 'Files', + SUPPORTPATH . 'Log', + ], true); + + $this->assertSame($expected, $publisher->getFiles()); + } + + //-------------------------------------------------------------------- + + public function testAddPathFile() + { + $publisher = new Publisher(SUPPORTPATH . 'Files'); + + $publisher->addPath('baker/banana.php'); + + $this->assertSame([$this->file], $publisher->getFiles()); + } + + public function testAddPathFileRecursiveDoesNothing() + { + $publisher = new Publisher(SUPPORTPATH . 'Files'); + + $publisher->addPath('baker/banana.php', true); + + $this->assertSame([$this->file], $publisher->getFiles()); + } + + public function testAddPathDirectory() + { + $publisher = new Publisher(SUPPORTPATH . 'Files'); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + ]; + + $publisher->addPath('able'); + + $this->assertSame($expected, $publisher->getFiles()); + } + + public function testAddPathDirectoryRecursive() + { + $publisher = new Publisher(SUPPORTPATH); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $publisher->addPath('Files'); + + $this->assertSame($expected, $publisher->getFiles()); + } + + public function testAddPaths() + { + $publisher = new Publisher(SUPPORTPATH . 'Files'); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $publisher->addPaths([ + 'able', + 'baker/banana.php', + ]); + + $this->assertSame($expected, $publisher->getFiles()); + } + + public function testAddPathsRecursive() + { + $publisher = new Publisher(SUPPORTPATH); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + SUPPORTPATH . 'Log/Handlers/TestHandler.php', + ]; + + $publisher->addPaths([ + 'Files', + 'Log', + ], true); + + $this->assertSame($expected, $publisher->getFiles()); + } + + //-------------------------------------------------------------------- + + public function testAddUri() + { + $publisher = new Publisher(); + $publisher->addUri('https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/composer.json'); + + $scratch = $this->getPrivateProperty($publisher, 'scratch'); + + $this->assertSame([$scratch . 'composer.json'], $publisher->getFiles()); + } + + public function testAddUris() + { + $publisher = new Publisher(); + $publisher->addUris([ + 'https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/LICENSE', + 'https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/composer.json', + ]); + + $scratch = $this->getPrivateProperty($publisher, 'scratch'); + + $this->assertSame([$scratch . 'LICENSE', $scratch . 'composer.json'], $publisher->getFiles()); + } + + //-------------------------------------------------------------------- + + public function testRemovePatternEmpty() + { + $publisher = new Publisher(); + $publisher->addDirectory(SUPPORTPATH . 'Files', true); + + $files = $publisher->getFiles(); + + $publisher->removePattern(''); + + $this->assertSame($files, $publisher->getFiles()); + } + + public function testRemovePatternRegex() + { + $publisher = new Publisher(); + $publisher->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + $this->directory . 'apple.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $publisher->removePattern('#[a-z]+_.*#'); + + $this->assertSame($expected, $publisher->getFiles()); + } + + public function testRemovePatternPseudo() + { + $publisher = new Publisher(); + $publisher->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + $this->directory . 'apple.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $publisher->removePattern('*_*.php'); + + $this->assertSame($expected, $publisher->getFiles()); + } + + public function testRemovePatternScope() + { + $publisher = new Publisher(); + $publisher->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $publisher->removePattern('*.php', $this->directory); + + $this->assertSame($expected, $publisher->getFiles()); + } + + //-------------------------------------------------------------------- + + public function testRetainPatternEmpty() + { + $publisher = new Publisher(); + $publisher->addDirectory(SUPPORTPATH . 'Files', true); + + $files = $publisher->getFiles(); + + $publisher->retainPattern(''); + + $this->assertSame($files, $publisher->getFiles()); + } + + public function testRetainPatternRegex() + { + $publisher = new Publisher(); + $publisher->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + ]; + + $publisher->retainPattern('#[a-z]+_.*#'); + + $this->assertSame($expected, $publisher->getFiles()); + } + + public function testRetainPatternPseudo() + { + $publisher = new Publisher(); + $publisher->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + $this->directory . 'fig_3.php', + ]; + + $publisher->retainPattern('*_?.php'); + + $this->assertSame($expected, $publisher->getFiles()); + } + + public function testRetainPatternScope() + { + $publisher = new Publisher(); + $publisher->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + $this->directory . 'fig_3.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $publisher->retainPattern('*_?.php', $this->directory); + + $this->assertSame($expected, $publisher->getFiles()); + } +} diff --git a/tests/system/Publisher/PublisherOutputTest.php b/tests/system/Publisher/PublisherOutputTest.php new file mode 100644 index 000000000000..5183d188d6b9 --- /dev/null +++ b/tests/system/Publisher/PublisherOutputTest.php @@ -0,0 +1,202 @@ +structure = [ + 'able' => [ + 'apple.php' => 'Once upon a midnight dreary', + 'bazam' => 'While I pondered weak and weary', + ], + 'boo' => [ + 'far' => 'Upon a tome of long-forgotten lore', + 'faz' => 'There came a tapping up on the door', + ], + 'AnEmptyFolder' => [], + 'simpleFile' => 'A tap-tap-tapping upon my door', + '.hidden' => 'There is no spoon', + ]; + + $this->root = vfsStream::setup('root', null, $this->structure); + } + + //-------------------------------------------------------------------- + + public function testCopy() + { + $publisher = new Publisher($this->directory, $this->root->url()); + $publisher->addFile($this->file); + + $this->assertFileDoesNotExist($this->root->url() . '/banana.php'); + + $result = $publisher->copy(false); + + $this->assertTrue($result); + $this->assertFileExists($this->root->url() . '/banana.php'); + } + + public function testCopyReplace() + { + $file = $this->directory . 'apple.php'; + $publisher = new Publisher($this->directory, $this->root->url() . '/able'); + $publisher->addFile($file); + + $this->assertFileExists($this->root->url() . '/able/apple.php'); + $this->assertFalse(same_file($file, $this->root->url() . '/able/apple.php')); + + $result = $publisher->copy(true); + + $this->assertTrue($result); + $this->assertTrue(same_file($file, $this->root->url() . '/able/apple.php')); + } + + public function testCopyIgnoresSame() + { + $publisher = new Publisher($this->directory, $this->root->url()); + $publisher->addFile($this->file); + + copy($this->file, $this->root->url() . '/banana.php'); + + $result = $publisher->copy(false); + $this->assertTrue($result); + + $result = $publisher->copy(true); + $this->assertTrue($result); + } + + public function testCopyIgnoresCollision() + { + $publisher = new Publisher($this->directory, $this->root->url()); + + mkdir($this->root->url() . '/banana.php'); + + $result = $publisher->addFile($this->file)->copy(false); + $errors = $publisher->getErrors(); + + $this->assertTrue($result); + $this->assertSame([], $errors); + } + + public function testCopyCollides() + { + $publisher = new Publisher($this->directory, $this->root->url()); + $expected = lang('Publisher.collision', ['dir', $this->file, $this->root->url() . '/banana.php']); + + mkdir($this->root->url() . '/banana.php'); + + $result = $publisher->addFile($this->file)->copy(true); + $errors = $publisher->getErrors(); + + $this->assertFalse($result); + $this->assertCount(1, $errors); + $this->assertSame([$this->file], array_keys($errors)); + $this->assertSame($expected, $errors[$this->file]->getMessage()); + } + + //-------------------------------------------------------------------- + + public function testMerge() + { + $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); + + $this->assertFileDoesNotExist($this->root->url() . '/able/fig_3.php'); + $this->assertDirectoryDoesNotExist($this->root->url() . '/baker'); + + $result = $publisher->addPath('/')->merge(false); + + $this->assertTrue($result); + $this->assertFileExists($this->root->url() . '/able/fig_3.php'); + $this->assertDirectoryExists($this->root->url() . '/baker'); + } + + public function testMergeReplace() + { + $this->assertFalse(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php')); + $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); + + $result = $publisher->addPath('/')->merge(true); + + $this->assertTrue($result); + $this->assertTrue(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php')); + } + + public function testMergeCollides() + { + $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); + $expected = lang('Publisher.collision', ['dir', $this->directory . 'fig_3.php', $this->root->url() . '/able/fig_3.php']); + + mkdir($this->root->url() . '/able/fig_3.php'); + + $result = $publisher->addPath('/')->merge(true); + $errors = $publisher->getErrors(); + + $this->assertFalse($result); + $this->assertCount(1, $errors); + $this->assertSame([$this->directory . 'fig_3.php'], array_keys($errors)); + $this->assertSame($expected, $errors[$this->directory . 'fig_3.php']->getMessage()); + } + + //-------------------------------------------------------------------- + + public function testPublish() + { + $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); + + $result = $publisher->publish(); + + $this->assertTrue($result); + $this->assertFileExists($this->root->url() . '/able/fig_3.php'); + $this->assertDirectoryExists($this->root->url() . '/baker'); + $this->assertTrue(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php')); + } +} diff --git a/tests/system/Publisher/PublisherSupportTest.php b/tests/system/Publisher/PublisherSupportTest.php new file mode 100644 index 000000000000..5592c02fc4ca --- /dev/null +++ b/tests/system/Publisher/PublisherSupportTest.php @@ -0,0 +1,199 @@ +assertCount(1, $result); + $this->assertInstanceOf(TestPublisher::class, $result[0]); + } + + public function testDiscoverNothing() + { + $result = Publisher::discover('Nothing'); + + $this->assertSame([], $result); + } + + public function testDiscoverStores() + { + $publisher = Publisher::discover()[0]; + $publisher->addFile($this->file); + + $result = Publisher::discover(); + $this->assertSame($publisher, $result[0]); + $this->assertSame([$this->file], $result[0]->getFiles()); + } + + //-------------------------------------------------------------------- + + public function testResolveDirectoryDirectory() + { + $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveDirectory'); + + $this->assertSame($this->directory, $method($this->directory)); + } + + public function testResolveDirectoryFile() + { + $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveDirectory'); + + $this->expectException(PublisherException::class); + $this->expectExceptionMessage(lang('Publisher.expectedDirectory', ['invokeArgs'])); + + $method($this->file); + } + + public function testResolveDirectorySymlink() + { + // Create a symlink to test + $link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4)); + symlink($this->directory, $link); + + $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveDirectory'); + + $this->assertSame($this->directory, $method($link)); + + unlink($link); + } + + //-------------------------------------------------------------------- + + public function testResolveFileFile() + { + $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveFile'); + + $this->assertSame($this->file, $method($this->file)); + } + + public function testResolveFileSymlink() + { + // Create a symlink to test + $link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4)); + symlink($this->file, $link); + + $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveFile'); + + $this->assertSame($this->file, $method($link)); + + unlink($link); + } + + public function testResolveFileDirectory() + { + $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveFile'); + + $this->expectException(PublisherException::class); + $this->expectExceptionMessage(lang('Publisher.expectedFile', ['invokeArgs'])); + + $method($this->directory); + } + + //-------------------------------------------------------------------- + + public function testGetScratch() + { + $publisher = new Publisher(); + $this->assertNull($this->getPrivateProperty($publisher, 'scratch')); + + $method = $this->getPrivateMethodInvoker($publisher, 'getScratch'); + $scratch = $method(); + + $this->assertIsString($scratch); + $this->assertDirectoryExists($scratch); + $this->assertDirectoryIsWritable($scratch); + $this->assertNotNull($this->getPrivateProperty($publisher, 'scratch')); + + // Directory and contents should be removed on __destruct() + $file = $scratch . 'obvious_statement.txt'; + file_put_contents($file, 'Bananas are a most peculiar fruit'); + + $publisher->__destruct(); + + $this->assertFileDoesNotExist($file); + $this->assertDirectoryDoesNotExist($scratch); + } + + public function testGetErrors() + { + $publisher = new Publisher(); + $this->assertSame([], $publisher->getErrors()); + + $expected = [ + $this->file => PublisherException::forCollision($this->file, $this->file), + ]; + + $this->setPrivateProperty($publisher, 'errors', $expected); + + $this->assertSame($expected, $publisher->getErrors()); + } + + //-------------------------------------------------------------------- + + public function testWipeDirectory() + { + $directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)); + mkdir($directory, 0700); + $this->assertDirectoryExists($directory); + + $method = $this->getPrivateMethodInvoker(Publisher::class, 'wipeDirectory'); + $method($directory); + + $this->assertDirectoryDoesNotExist($directory); + } + + public function testWipeIgnoresFiles() + { + $method = $this->getPrivateMethodInvoker(Publisher::class, 'wipeDirectory'); + $method($this->file); + + $this->assertFileExists($this->file); + } + + public function testWipe() + { + $directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)); + mkdir($directory, 0700); + $this->assertDirectoryExists($directory); + + $publisher = new Publisher($this->directory, $directory); + $publisher->wipe(); + + $this->assertDirectoryDoesNotExist($directory); + } +} From 291594c041d334722702b224e98405974c66baf2 Mon Sep 17 00:00:00 2001 From: MGatner Date: Mon, 24 May 2021 15:51:16 -0400 Subject: [PATCH 012/490] Apply suggestions from code review Co-authored-by: Mostafa Khudair <59371810+mostafakhudair@users.noreply.github.com> --- system/Language/en/Publisher.php | 4 +- .../Exceptions/PublisherException.php | 10 +- system/Publisher/Publisher.php | 125 ++++++++---------- 3 files changed, 60 insertions(+), 79 deletions(-) diff --git a/system/Language/en/Publisher.php b/system/Language/en/Publisher.php index ada599074efe..cb5ce81f10a4 100644 --- a/system/Language/en/Publisher.php +++ b/system/Language/en/Publisher.php @@ -11,7 +11,7 @@ // Publisher language settings return [ - 'expectedFile' => 'Publisher::{0} expects a valid file.', - 'expectedDirectory' => 'Publisher::{0} expects a valid directory.', 'collision' => 'Publisher encountered an unexpected {0} while copying {1} to {2}.', + 'expectedDirectory' => 'Publisher::{0} expects a valid directory.', + 'expectedFile' => 'Publisher::{0} expects a valid file.', ]; diff --git a/system/Publisher/Exceptions/PublisherException.php b/system/Publisher/Exceptions/PublisherException.php index 23a4b54d3e57..81f9bb4b7174 100644 --- a/system/Publisher/Exceptions/PublisherException.php +++ b/system/Publisher/Exceptions/PublisherException.php @@ -15,6 +15,11 @@ class PublisherException extends FrameworkException { + public static function forCollision(string $from, string $to) + { + return new static(lang('Publisher.collision', [filetype($to), $from, $to])); + } + public static function forExpectedDirectory(string $caller) { return new static(lang('Publisher.expectedDirectory', [$caller])); @@ -24,9 +29,4 @@ public static function forExpectedFile(string $caller) { return new static(lang('Publisher.expectedFile', [$caller])); } - - public static function forCollision(string $from, string $to) - { - return new static(lang('Publisher.collision', [filetype($to), $from, $to])); - } } diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index d7f797e00312..b2e9de8d577b 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -20,24 +20,18 @@ /** * Publisher Class * - * Publishers read in file paths from a variety - * of sources and copy the files out to different - * destinations. - * This class acts both as a base for individual - * publication directives as well as the mode of - * discovery for said instances. - * In this class a "file" is a full path to a - * verified file while a "path" is relative to - * to its source or destination and may indicate - * either a file or directory fo unconfirmed - * existence. - * Class failures throw a PublisherException, - * but some underlying methods may percolate - * different exceptions, like FileException, - * FileNotFoundException, InvalidArgumentException. - * Write operations will catch all errors in the - * file-specific $errors property to minimize - * impact of partial batch operations. + * Publishers read in file paths from a variety of sources and copy + * the files out to different destinations. This class acts both as + * a base for individual publication directives as well as the mode + * of discovery for said instances. In this class a "file" is a full + * path to a verified file while a "path" is relative to its source + * or destination and may indicate either a file or directory of + * unconfirmed existence. + * class failures throw the PublisherException, but some underlying + * methods may percolate different exceptions, like FileException, + * FileNotFoundException or InvalidArgumentException. + * Write operations will catch all errors in the file-specific + * $errors property to minimize impact of partial batch operations. */ class Publisher { @@ -90,14 +84,17 @@ class Publisher * Discovers and returns all Publishers * in the specified namespace directory. * + * @param string $directory + * * @return self[] */ - public static function discover($directory = 'Publishers'): array + public static function discover(string $directory = 'Publishers'): array { if (isset(self::$discovered[$directory])) { return self::$discovered[$directory]; } + self::$discovered[$directory] = []; /** @var FileLocator $locator */ @@ -145,8 +142,7 @@ private static function resolveDirectory(string $directory): string } /** - * Resolves a full path and verifies - * it is an actual file. + * Resolves a full path and verifies it is an actual file. * * @param string $file * @@ -184,11 +180,10 @@ private static function filterFiles(array $files, string $directory): array } /** - * Returns any files whose basename matches - * the given pattern. + * Returns any files whose `basename` matches the given pattern. * - * @param string[] $files - * @param string $pattern Regex or pseudo-regex string + * @param array $files + * @param string $pattern Regex or pseudo-regex string * * @return string[] */ @@ -239,13 +234,12 @@ private static function wipeDirectory(string $directory) } /** - * Copies a file with directory creation - * and identical file awareness. + * Copies a file with directory creation and identical file awareness. * Intentionally allows errors. * - * @param string $from - * @param string $to - * @param bool $replace + * @param string $from + * @param string $to + * @param boolean $replace * * @return void * @@ -301,8 +295,7 @@ public function __construct(string $source = null, string $destination = null) } /** - * Cleans up any temporary files - * in the scratch space. + * Cleans up any temporary files in the scratch space. */ public function __destruct() { @@ -315,14 +308,13 @@ public function __destruct() } /** - * Reads in file sources and copies out - * the files to their destinations. - * This method should be reimplemented by - * child classes intended for discovery. + * Reads files in the sources and copies them out to their destinations. + * This method should be reimplemented by child classes intended for + * discovery. * - * @return bool + * @return boolean */ - public function publish() + public function publish(): bool { return $this->addPath('/')->merge(true); } @@ -347,8 +339,7 @@ protected function getScratch(): string } /** - * Returns any errors from the last - * write operation. + * Returns errors from the last write operation if any. * * @return array */ @@ -358,8 +349,7 @@ public function getErrors(): array } /** - * Optimizes and returns the - * current file list. + * Optimizes and returns the current file list. * * @return string[] */ @@ -372,11 +362,10 @@ public function getFiles(): array } /** - * Sets the file list directly. - * Files are still subject to verification. + * Sets the file list directly, files are still subject to verification. * This works as a "reset" method with []. * - * @param string[] $files A new file list to use + * @param string[] $files The new file list to use * * @return $this */ @@ -407,8 +396,7 @@ public function addFiles(array $files) } /** - * Verifies and adds a single file - * to the file list. + * Verifies and adds a single file to the file list. * * @param string $file * @@ -469,11 +457,10 @@ public function addDirectories(array $directories, bool $recursive = false) } /** - * Verifies and adds all files - * from a directory. + * Verifies and adds all files from a directory. * - * @param string $directory - * @param bool $recursive + * @param string $directory + * @param boolean $recursive * * @return $this */ @@ -520,8 +507,8 @@ public function addPaths(array $paths, bool $recursive = true) /** * Adds a single path to the file list. * - * @param string $path - * @param bool $recursive + * @param string $path + * @param boolean $recursive * * @return $this */ @@ -563,8 +550,7 @@ public function addUris(array $uris) } /** - * Downloads a file from the URI - * and adds it to the file list. + * Downloads a file from the URI, and adds it to the file list. * * @param string $uri Because HTTP\URI is stringable it will still be accepted * @@ -576,8 +562,7 @@ public function addUri(string $uri) $file = $this->getScratch() . basename((new URI($uri))->getPath()); // Get the content and write it to the scratch space - $response = service('curlrequest')->get($uri); - write_file($file, $response->getBody()); + write_file($file, service('curlrequest')->get($uri)->getBody()); return $this->addFile($file); } @@ -585,11 +570,11 @@ public function addUri(string $uri) //-------------------------------------------------------------------- /** - * Removes any files from the list that match - * the supplied pattern (within the optional scope). + * Removes any files from the list that match the supplied pattern + * (within the optional scope). * - * @param string $pattern Regex or pseudo-regex string - * @param string|null $scope A directory to limit the scope + * @param string $pattern Regex or pseudo-regex string + * @param string|null $scope The directory to limit the scope * * @return $this */ @@ -601,9 +586,7 @@ public function removePattern(string $pattern, string $scope = null) } // Start with all files or those in scope - $files = is_null($scope) - ? $this->files - : self::filterFiles($this->files, $scope); + $files = is_null($scope) ? $this->files : self::filterFiles($this->files, $scope); // Remove any files that match the pattern return $this->removeFiles(self::matchFiles($files, $pattern)); @@ -611,9 +594,9 @@ public function removePattern(string $pattern, string $scope = null) /** * Keeps only the files from the list that match - * the supplied pattern (within the optional scope). + * (within the optional scope). * - * @param string $pattern Regex or pseudo-regex string + * @param string $pattern Regex or pseudo-regex string * @param string|null $scope A directory to limit the scope * * @return $this @@ -654,10 +637,9 @@ public function wipe() //-------------------------------------------------------------------- /** - * Copies all files into the destination. - * Does not create directory structure. + * Copies all files into the destination, does not create directory structure. * - * @param bool $replace Whether to overwrite existing files. + * @param boolean $replace Whether to overwrite existing files. * * @return boolean Whether all files were copied successfully */ @@ -684,10 +666,9 @@ public function copy(bool $replace = true): bool /** * Merges all files into the destination. - * Creates a mirrored directory structure - * only for files from source. + * Creates a mirrored directory structure only for files from source. * - * @param bool $replace Whether to overwrite existing files. + * @param boolean $replace Whether to overwrite existing files. * * @return boolean Whether all files were copied successfully */ From c18a08fae8102dbbae8ebed7250218d31b86a074 Mon Sep 17 00:00:00 2001 From: MGatner Date: Tue, 25 May 2021 16:13:09 +0000 Subject: [PATCH 013/490] Apply additional suggestions --- system/Publisher/Publisher.php | 48 ++++++++------------- tests/_support/Publishers/TestPublisher.php | 2 +- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index b2e9de8d577b..640dd779756e 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -43,8 +43,7 @@ class Publisher private static $discovered = []; /** - * Directory to use for methods - * that need temporary storage. + * Directory to use for methods that need temporary storage. * Created on-the-fly as needed. * * @var string|null @@ -59,8 +58,7 @@ class Publisher private $files = []; /** - * Exceptions for specific files - * from the last write operation. + * Exceptions for specific files from the last write operation. * * @var array */ @@ -81,8 +79,7 @@ class Publisher protected $destination = FCPATH; /** - * Discovers and returns all Publishers - * in the specified namespace directory. + * Discovers and returns all Publishers in the specified namespace directory. * * @param string $directory * @@ -115,6 +112,7 @@ public static function discover(string $directory = 'Publishers'): array self::$discovered[$directory][] = new $className(); } } + sort(self::$discovered[$directory]); return self::$discovered[$directory]; @@ -123,8 +121,7 @@ public static function discover(string $directory = 'Publishers'): array //-------------------------------------------------------------------- /** - * Resolves a full path and verifies - * it is an actual directory. + * Resolves a full path and verifies it is an actual directory. * * @param string $directory * @@ -162,11 +159,10 @@ private static function resolveFile(string $file): string //-------------------------------------------------------------------- /** - * Filters an array of files, removing files not - * part of the given directory (recursive). + * Removes files that are not part of the given directory (recursive). * * @param string[] $files - * @param string $directory + * @param string $directory * * @return string[] */ @@ -207,15 +203,14 @@ private static function matchFiles(array $files, string $pattern) //-------------------------------------------------------------------- - /** - * Removes a directory and all its files - * and subdirectories. + /* + * Removes a directory and all its files and subdirectories. * * @param string $directory * * @return void */ - private static function wipeDirectory(string $directory) + private static function wipeDirectory(string $directory): void { if (is_dir($directory)) { @@ -267,8 +262,7 @@ private static function safeCopyFile(string $from, string $to, bool $replace): v } // Make sure the directory exists - $directory = pathinfo($to, PATHINFO_DIRNAME); - if (! is_dir($directory)) + if (! is_dir($directory = pathinfo($to, PATHINFO_DIRNAME))) { mkdir($directory, 0775, true); } @@ -280,8 +274,7 @@ private static function safeCopyFile(string $from, string $to, bool $replace): v //-------------------------------------------------------------------- /** - * Loads the helper and verifies the - * source and destination directories. + * Loads the helper and verifies the source and destination directories. * * @param string|null $source * @param string|null $destination @@ -322,8 +315,7 @@ public function publish(): bool //-------------------------------------------------------------------- /** - * Returns the temporary workspace, - * creating it if necessary. + * Returns the temporary workspace, creating it if necessary. * * @return string */ @@ -532,8 +524,7 @@ public function addPath(string $path, bool $recursive = true) //-------------------------------------------------------------------- /** - * Downloads and stages files from - * an array of URIs. + * Downloads and stages files from an array of URIs. * * @param string[] $uris * @@ -609,15 +600,10 @@ public function retainPattern(string $pattern, string $scope = null) } // Start with all files or those in scope - $files = is_null($scope) - ? $this->files - : self::filterFiles($this->files, $scope); - - // Match the pattern within the scoped files - $matched = self::matchFiles($files, $pattern); + $files = is_null($scope) ? $this->files : self::filterFiles($this->files, $scope); - // ... and remove their inverse - return $this->removeFiles(array_diff($files, $matched)); + // Matches the pattern within the scoped files and remove their inverse. + return $this->removeFiles(array_diff($files, self::matchFiles($files, $pattern))); } //-------------------------------------------------------------------- diff --git a/tests/_support/Publishers/TestPublisher.php b/tests/_support/Publishers/TestPublisher.php index 8ef30ca90024..fdb3547478d1 100644 --- a/tests/_support/Publishers/TestPublisher.php +++ b/tests/_support/Publishers/TestPublisher.php @@ -9,7 +9,7 @@ class TestPublisher extends Publisher /** * Runs the defined Operations. */ - public function publish() + public function publish(): bool { $this->downloadFromUrls($urls)->mergeToDirectory(FCPATH . 'assets'); } From ea6ca9fec0b2a74d789b2b6b3e2961c567f46544 Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 27 May 2021 20:15:37 +0000 Subject: [PATCH 014/490] Add Guide, tweak class --- system/Publisher/Publisher.php | 95 +++- tests/_support/Publishers/TestPublisher.php | 55 ++- .../system/Publisher/PublisherOutputTest.php | 26 +- .../system/Publisher/PublisherSupportTest.php | 17 +- user_guide_src/source/libraries/publisher.rst | 437 ++++++++++++++++++ 5 files changed, 598 insertions(+), 32 deletions(-) create mode 100644 user_guide_src/source/libraries/publisher.rst diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index 640dd779756e..b9e5cca24e90 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -15,6 +15,7 @@ use CodeIgniter\Files\File; use CodeIgniter\HTTP\URI; use CodeIgniter\Publisher\Exceptions\PublisherException; +use RuntimeException; use Throwable; /** @@ -64,6 +65,13 @@ class Publisher */ private $errors = []; + /** + * List of file published curing the last write operation. + * + * @var string[] + */ + private $published = []; + /** * Base path to use for the source. * @@ -85,7 +93,7 @@ class Publisher * * @return self[] */ - public static function discover(string $directory = 'Publishers'): array + final public static function discover(string $directory = 'Publishers'): array { if (isset(self::$discovered[$directory])) { @@ -301,7 +309,7 @@ public function __destruct() } /** - * Reads files in the sources and copies them out to their destinations. + * Reads files from the sources and copies them out to their destinations. * This method should be reimplemented by child classes intended for * discovery. * @@ -309,17 +317,42 @@ public function __destruct() */ public function publish(): bool { + if ($this->source === ROOTPATH && $this->destination === FCPATH) + { + throw new RuntimeException('Child classes of Publisher should provide their own source and destination or publish method.'); + } + return $this->addPath('/')->merge(true); } //-------------------------------------------------------------------- + /** + * Returns the source directory. + * + * @return string + */ + final public function getSource(): string + { + return $this->source; + } + + /** + * Returns the destination directory. + * + * @return string + */ + final public function getDestination(): string + { + return $this->destination; + } + /** * Returns the temporary workspace, creating it if necessary. * * @return string */ - protected function getScratch(): string + final public function getScratch(): string { if (is_null($this->scratch)) { @@ -335,17 +368,27 @@ protected function getScratch(): string * * @return array */ - public function getErrors(): array + final public function getErrors(): array { return $this->errors; } + /** + * Returns the files published by the last write operation. + * + * @return string[] + */ + final public function getPublished(): array + { + return $this->published; + } + /** * Optimizes and returns the current file list. * * @return string[] */ - public function getFiles(): array + final public function getFiles(): array { $this->files = array_unique($this->files, SORT_STRING); sort($this->files, SORT_STRING); @@ -353,6 +396,8 @@ public function getFiles(): array return $this->files; } + //-------------------------------------------------------------------- + /** * Sets the file list directly, files are still subject to verification. * This works as a "reset" method with []. @@ -361,15 +406,13 @@ public function getFiles(): array * * @return $this */ - public function setFiles(array $files) + final public function setFiles(array $files) { $this->files = []; return $this->addFiles($files); } - //-------------------------------------------------------------------- - /** * Verifies and adds files to the list. * @@ -377,7 +420,7 @@ public function setFiles(array $files) * * @return $this */ - public function addFiles(array $files) + final public function addFiles(array $files) { foreach ($files as $file) { @@ -394,7 +437,7 @@ public function addFiles(array $files) * * @return $this */ - public function addFile(string $file) + final public function addFile(string $file) { $this->files[] = self::resolveFile($file); @@ -408,7 +451,7 @@ public function addFile(string $file) * * @return $this */ - public function removeFiles(array $files) + final public function removeFiles(array $files) { $this->files = array_diff($this->files, $files); @@ -422,7 +465,7 @@ public function removeFiles(array $files) * * @return $this */ - public function removeFile(string $file) + final public function removeFile(string $file) { return $this->removeFiles([$file]); } @@ -438,7 +481,7 @@ public function removeFile(string $file) * * @return $this */ - public function addDirectories(array $directories, bool $recursive = false) + final public function addDirectories(array $directories, bool $recursive = false) { foreach ($directories as $directory) { @@ -456,7 +499,7 @@ public function addDirectories(array $directories, bool $recursive = false) * * @return $this */ - public function addDirectory(string $directory, bool $recursive = false) + final public function addDirectory(string $directory, bool $recursive = false) { $directory = self::resolveDirectory($directory); @@ -486,7 +529,7 @@ public function addDirectory(string $directory, bool $recursive = false) * * @return $this */ - public function addPaths(array $paths, bool $recursive = true) + final public function addPaths(array $paths, bool $recursive = true) { foreach ($paths as $path) { @@ -504,7 +547,7 @@ public function addPaths(array $paths, bool $recursive = true) * * @return $this */ - public function addPath(string $path, bool $recursive = true) + final public function addPath(string $path, bool $recursive = true) { $full = $this->source . $path; @@ -530,7 +573,7 @@ public function addPath(string $path, bool $recursive = true) * * @return $this */ - public function addUris(array $uris) + final public function addUris(array $uris) { foreach ($uris as $uri) { @@ -547,7 +590,7 @@ public function addUris(array $uris) * * @return $this */ - public function addUri(string $uri) + final public function addUri(string $uri) { // Figure out a good filename (using URI strips queries and fragments) $file = $this->getScratch() . basename((new URI($uri))->getPath()); @@ -569,7 +612,7 @@ public function addUri(string $uri) * * @return $this */ - public function removePattern(string $pattern, string $scope = null) + final public function removePattern(string $pattern, string $scope = null) { if ($pattern === '') { @@ -592,7 +635,7 @@ public function removePattern(string $pattern, string $scope = null) * * @return $this */ - public function retainPattern(string $pattern, string $scope = null) + final public function retainPattern(string $pattern, string $scope = null) { if ($pattern === '') { @@ -613,7 +656,7 @@ public function retainPattern(string $pattern, string $scope = null) * * @return $this */ - public function wipe() + final public function wipe() { self::wipeDirectory($this->destination); @@ -629,9 +672,9 @@ public function wipe() * * @return boolean Whether all files were copied successfully */ - public function copy(bool $replace = true): bool + final public function copy(bool $replace = true): bool { - $this->errors = []; + $this->errors = $this->published = []; foreach ($this->getFiles() as $file) { @@ -640,6 +683,7 @@ public function copy(bool $replace = true): bool try { self::safeCopyFile($file, $to, $replace); + $this->published[] = $to; } catch (Throwable $e) { @@ -658,9 +702,9 @@ public function copy(bool $replace = true): bool * * @return boolean Whether all files were copied successfully */ - public function merge(bool $replace = true): bool + final public function merge(bool $replace = true): bool { - $this->errors = []; + $this->errors = $this->published = []; // Get the file from source for special handling $sourced = self::filterFiles($this->getFiles(), $this->source); @@ -678,6 +722,7 @@ public function merge(bool $replace = true): bool try { self::safeCopyFile($file, $to, $replace); + $this->published[] = $to; } catch (Throwable $e) { diff --git a/tests/_support/Publishers/TestPublisher.php b/tests/_support/Publishers/TestPublisher.php index fdb3547478d1..9192c3899b83 100644 --- a/tests/_support/Publishers/TestPublisher.php +++ b/tests/_support/Publishers/TestPublisher.php @@ -4,13 +4,62 @@ use CodeIgniter\Publisher\Publisher; -class TestPublisher extends Publisher +final class TestPublisher extends Publisher { /** - * Runs the defined Operations. + * Fakes an error on the given file. + * + * @return $this + */ + public static function setError(string $file) + { + self::$error = $file; + } + + /** + * A file to cause an error + * + * @var string + */ + private static $error = ''; + + /** + * Base path to use for the source. + * + * @var string + */ + protected $source = SUPPORTPATH . 'Files'; + + /** + * Base path to use for the destination. + * + * @var string + */ + protected $destination = WRITEPATH; + + /** + * Fakes a publish event so no files are actually copied. */ public function publish(): bool { - $this->downloadFromUrls($urls)->mergeToDirectory(FCPATH . 'assets'); + $this->errors = $this->published = []; + + $this->addPath(''); + + // Copy each sourced file to its relative destination + foreach ($this->getFiles() as $file) + { + if ($file === self::$error) + { + $this->errors[$file] = new RuntimeException('Have an error, dear.'); + } + else + { + // Resolve the destination path + $this->published[] = $this->destination . substr($file, strlen($this->source)); + } + } + + return $this->errors === []; } } diff --git a/tests/system/Publisher/PublisherOutputTest.php b/tests/system/Publisher/PublisherOutputTest.php index 5183d188d6b9..0f416440b803 100644 --- a/tests/system/Publisher/PublisherOutputTest.php +++ b/tests/system/Publisher/PublisherOutputTest.php @@ -112,6 +112,7 @@ public function testCopyIgnoresSame() $result = $publisher->copy(true); $this->assertTrue($result); + $this->assertSame([$this->root->url() . '/banana.php'], $publisher->getPublished()); } public function testCopyIgnoresCollision() @@ -121,10 +122,10 @@ public function testCopyIgnoresCollision() mkdir($this->root->url() . '/banana.php'); $result = $publisher->addFile($this->file)->copy(false); - $errors = $publisher->getErrors(); $this->assertTrue($result); - $this->assertSame([], $errors); + $this->assertSame([], $publisher->getErrors()); + $this->assertSame([$this->root->url() . '/banana.php'], $publisher->getPublished()); } public function testCopyCollides() @@ -140,6 +141,7 @@ public function testCopyCollides() $this->assertFalse($result); $this->assertCount(1, $errors); $this->assertSame([$this->file], array_keys($errors)); + $this->assertSame([], $publisher->getPublished()); $this->assertSame($expected, $errors[$this->file]->getMessage()); } @@ -148,6 +150,12 @@ public function testCopyCollides() public function testMerge() { $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); + $expected = [ + $this->root->url() . '/able/apple.php', + $this->root->url() . '/able/fig_3.php', + $this->root->url() . '/able/prune_ripe.php', + $this->root->url() . '/baker/banana.php', + ]; $this->assertFileDoesNotExist($this->root->url() . '/able/fig_3.php'); $this->assertDirectoryDoesNotExist($this->root->url() . '/baker'); @@ -157,23 +165,36 @@ public function testMerge() $this->assertTrue($result); $this->assertFileExists($this->root->url() . '/able/fig_3.php'); $this->assertDirectoryExists($this->root->url() . '/baker'); + $this->assertSame($expected, $publisher->getPublished()); } public function testMergeReplace() { $this->assertFalse(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php')); $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); + $expected = [ + $this->root->url() . '/able/apple.php', + $this->root->url() . '/able/fig_3.php', + $this->root->url() . '/able/prune_ripe.php', + $this->root->url() . '/baker/banana.php', + ]; $result = $publisher->addPath('/')->merge(true); $this->assertTrue($result); $this->assertTrue(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php')); + $this->assertSame($expected, $publisher->getPublished()); } public function testMergeCollides() { $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); $expected = lang('Publisher.collision', ['dir', $this->directory . 'fig_3.php', $this->root->url() . '/able/fig_3.php']); + $published = [ + $this->root->url() . '/able/apple.php', + $this->root->url() . '/able/prune_ripe.php', + $this->root->url() . '/baker/banana.php', + ]; mkdir($this->root->url() . '/able/fig_3.php'); @@ -183,6 +204,7 @@ public function testMergeCollides() $this->assertFalse($result); $this->assertCount(1, $errors); $this->assertSame([$this->directory . 'fig_3.php'], array_keys($errors)); + $this->assertSame($published, $publisher->getPublished()); $this->assertSame($expected, $errors[$this->directory . 'fig_3.php']->getMessage()); } diff --git a/tests/system/Publisher/PublisherSupportTest.php b/tests/system/Publisher/PublisherSupportTest.php index 5592c02fc4ca..f17be3460432 100644 --- a/tests/system/Publisher/PublisherSupportTest.php +++ b/tests/system/Publisher/PublisherSupportTest.php @@ -126,13 +126,26 @@ public function testResolveFileDirectory() //-------------------------------------------------------------------- + public function testGetSource() + { + $publisher = new Publisher(ROOTPATH); + + $this->assertSame(ROOTPATH, $publisher->getSource()); + } + + public function testGetDestination() + { + $publisher = new Publisher(ROOTPATH, SUPPORTPATH); + + $this->assertSame(SUPPORTPATH, $publisher->getDestination()); + } + public function testGetScratch() { $publisher = new Publisher(); $this->assertNull($this->getPrivateProperty($publisher, 'scratch')); - $method = $this->getPrivateMethodInvoker($publisher, 'getScratch'); - $scratch = $method(); + $scratch = $publisher->getScratch(); $this->assertIsString($scratch); $this->assertDirectoryExists($scratch); diff --git a/user_guide_src/source/libraries/publisher.rst b/user_guide_src/source/libraries/publisher.rst new file mode 100644 index 000000000000..ef97e0b52f95 --- /dev/null +++ b/user_guide_src/source/libraries/publisher.rst @@ -0,0 +1,437 @@ +######### +Publisher +######### + +The Publisher library provides a means to copy files within a project using robust detection and error checking. + +.. contents:: + :local: + :depth: 2 + +******************* +Loading the Library +******************* + +Because Publisher instances are specific to their source and destination this library is not available +through ``Services`` but should be instantiated or extended directly. E.g. + + $publisher = new \CodeIgniter\Publisher\Publisher(); + +***************** +Concept and Usage +***************** + +``Publisher`` solves a handful of common problems when working within a backend framework: + +* How do I maintain project assets with version dependencies? +* How do I manage uploads and other "dynamic" files that need to be web accessible? +* How can I update my project when the framework or modules change? +* How can components inject new content into existing projects? + +At its most basic, publishing amounts to copying a file or files into a project. ``Publisher`` uses fluent-style +command chaining to read, filter, and process input files, then copies or merges them into the target destination. +You may use ``Publisher`` on demand in your Controllers or other components, or you may stage publications by extending +the class and leveraging its discovery with ``spark publish``. + +On Demand +========= + +Access ``Publisher`` directly by instantiating a new instance of the class:: + + $publisher = new \CodeIgniter\Publisher\Publisher(); + +By default the source and destination will be set to ``ROOTPATH`` and ``FCPATH`` respectively, giving ``Publisher`` +easy access to take any file from your project and make it web-accessible. Alternatively you may pass a new source +or source and destination into the constructor:: + + $vendorPublisher = new Publisher(ROOTPATH . 'vendor'); + $filterPublisher = new Publisher('/path/to/module/Filters', APPPATH . 'Filters'); + +Once the source and destination are set you may start adding relative input files:: + + $frameworkPublisher = new Publisher(ROOTPATH . 'vendor/codeigniter4/codeigniter4'); + + // All "path" commands are relative to $source + $frameworkPublisher->addPath('app/Config/Cookie.php'); + + // You may also add from outside the source, but the files will not be merged into subdirectories + $frameworkPublisher->addFiles([ + '/opt/mail/susan', + '/opt/mail/ubuntu', + ]); + $frameworkPublisher->addDirectory(SUPPORTPATH . 'Images'); + +Once all the files are staged use one of the output commands (**copy()** or **merge()**) to process the staged files +to their destination(s):: + + // Place all files into $destination + $frameworkPublisher->copy(); + + // Place all files into $destination, overwriting existing files + $frameworkPublisher->copy(true); + + // Place files into their relative $destination directories, overwriting and saving the boolean result + $result = $frameworkPublisher->merge(true); + +See the Library Reference for a full description of available methods. + +Automation and Discovery +======================== + +You may have regular publication tasks embedded as part of your application deployment or upkeep. ``Publisher`` leverages +the powerful ``Autoloader`` to locate any child classes primed for publication:: + + use CodeIgniter\CLI\CLI; + use CodeIgniter\Publisher\Publisher; + + foreach (Publisher::discover() as $publisher) + { + $result = $publisher->publish(); + + if ($result === false) + { + CLI::write(get_class($publisher) . ' failed to publish!', 'red'); + } + } + +By default ``discover()`` will search for the "Publishers" directory across all namespaces, but you may specify a +different directory and it will return any child classes found:: + + $memePublishers = Publisher::discover('CatGIFs'); + +Most of the time you will not need to handle your own discovery, just use the provided "publish" command:: + + > php spark publish + +By default on your class extension ``publish()`` will add all files from your ``$source`` and merge them +out to your destination, overwriting on collision. + +******** +Examples +******** + +Here are a handful of example use cases and their implementations to help you get started publishing. + +File Sync Example +================= + +You want to display a "photo of the day" image on your homepage. You have a feed for daily photos but you +need to get the actual file into a browsable location in your project at **public/images/daily_photo.jpg**. +You can set up :doc:`Custom Command ` to run daily that will handle this for you:: + + namespace App\Commands; + + use CodeIgniter\CLI\BaseCommand; + use CodeIgniter\Publisher\Publisher; + use Throwable; + + class DailyPhoto extends BaseCommand + { + protected $group = 'Publication'; + protected $name = 'publish:daily'; + protected $description = 'Publishes the latest daily photo to the homepage.'; + + public function run(array $params) + { + $publisher = new Publisher('/path/to/photos/', FCPATH . 'assets/images'); + + try + { + $publisher->addPath('daily_photo.jpg')->copy($replace = true); + } + catch (Throwable $e) + { + $this->showError($e); + } + } + } + +Now running ``spark publish:daily`` will keep your homepage's image up-to-date. What if the photo is +coming from an external API? You can use ``addUri()`` in place of ``addPath()`` to download the remote +resource and publish it out instead:: + + $publisher->addUri('https://example.com/feeds/daily_photo.jpg')->copy($replace = true); + +Asset Dependencies Example +========================== + +You want to integrate the frontend library "Bootstrap" into your project, but the frequent updates makes it a hassle +to keep up with. You can create a publication definition in your project to sync frontend assets by adding extending +``Publisher`` in your project. So **app/Publishers/BootstrapPublisher.php** might look like this:: + + namespace App\Publishers; + + use CodeIgniter\Publisher\Publisher; + + class BootstrapPublisher extends Publisher + { + /** + * Tell Publisher where to get the files. + * Since we will use Composer to download + * them we point to the "vendor" directory. + * + * @var string + */ + protected $source = 'vendor/twbs/bootstrap/'; + + /** + * FCPATH is always the default destination, + * but we may want them to go in a sub-folder + * to keep things organized. + * + * @var string + */ + protected $destination = FCPATH . 'bootstrap'; + + /** + * Use the "publish" method to indicate that this + * class is ready to be discovered and automated. + * + * @return boolean + */ + public function publish(): bool + { + return $this + // Add all the files relative to $source + ->addPath('dist') + + // Indicate we only want the minimized versions + ->retainPattern('*.min.*) + + // Merge-and-replace to retain the original directory structure + ->merge(true); + } + +Now add the dependency via Composer and call ``spark publish`` to run the publication:: + + > composer require twbs/bootstrap + > php spark publish + +... and you'll end up with something like this: + + public/.htaccess + public/favicon.ico + public/index.php + public/robots.txt + public/ + bootstrap/ + css/ + bootstrap.min.css + bootstrap-utilities.min.css.map + bootstrap-grid.min.css + bootstrap.rtl.min.css + bootstrap.min.css.map + bootstrap-reboot.min.css + bootstrap-utilities.min.css + bootstrap-reboot.rtl.min.css + bootstrap-grid.min.css.map + js/ + bootstrap.esm.min.js + bootstrap.bundle.min.js.map + bootstrap.bundle.min.js + bootstrap.min.js + bootstrap.esm.min.js.map + bootstrap.min.js.map + +Module Deployment Example +========================= + +You want to allow developers using your popular authentication module the ability to expand on the default behavior +of your Migration, Controller, and Model. You can create your own module "publish" command to inject these components +into an application for use:: + + namespace Math\Auth\Commands; + + use CodeIgniter\CLI\BaseCommand; + use CodeIgniter\Publisher\Publisher; + use Throwable; + + class Publish extends BaseCommand + { + protected $group = 'Auth'; + protected $name = 'auth:publish'; + protected $description = 'Publish Auth components into the current application.'; + + public function run(array $params) + { + // Use the Autoloader to figure out the module path + $source = service('autoloader')->getNamespace('Math\\Auth'); + + $publisher = new Publisher($source, APPATH); + + try + { + // Add only the desired components + $publisher->addPaths([ + 'Controllers', + 'Database/Migrations', + 'Models', + ])->merge(false); // Be careful not to overwrite anything + } + catch (Throwable $e) + { + $this->showError($e); + return; + } + + // If publication succeeded then update namespaces + foreach ($publisher->getFiles as $original) + { + // Get the location of the new file + $file = str_replace($source, APPPATH, $original); + + // Replace the namespace + $contents = file_get_contents($file); + $contents = str_replace('namespace Math\\Auth', 'namespace ' . APP_NAMESPACE, ); + file_put_contents($file, $contents); + } + } + } + +Now when your module users run ``php spark auth:publish`` they will have the following added to their project:: + + app/Controllers/AuthController.php + app/Database/Migrations/2017-11-20-223112_create_auth_tables.php.php + app/Models/LoginModel.php + app/Models/UserModel.php + +***************** +Library Reference +***************** + +Support Methods +=============== + +**[static] discover(string $directory = 'Publishers'): Publisher[]** + +Discovers and returns all Publishers in the specified namespace directory. For example, if both +**app/Publishers/FrameworkPublisher.php** and **myModule/src/Publishers/AssetPublisher.php** exist and are +extensions of ``Publisher`` then ``Publisher::discover()`` would return an instance of each. + +**publish(): bool** + +Processes the full input-process-output chain. By default this is the equivalent of calling ``addPath($source)`` +and ``merge(true)`` but child classes will typically provide their own implementation. ``publish()`` is called +on all discovered Publishers when running ``spark publish``. +Returns success or failure. + +**getScratch(): string** + +Returns the temporary workspace, creating it if necessary. Some operations use intermediate storage to stage +files and changes, and this provides the path to a transient, writable directory that you may use as well. + +**getErrors(): array** + +Returns any errors from the last write operation. The array keys are the files that caused the error, and the +values are the Throwable that was caught. Use ``getMessage()`` on the Throwable to get the error message. + +**getFiles(): string[]** + +Returns an array of all the loaded input files. + +Inputting Files +=============== + +**setFiles(array $files)** + +Sets the list of input files to the provided string array of file paths. + +**addFile(string $file)** +**addFiles(array $files)** + +Adds the file or files to the current list of input files. Files are absolute paths to actual files. + +**removeFile(string $file)** +**removeFiles(array $files)** + +Removes the file or files from the current list of input files. + +**addDirectory(string $directory, bool $recursive = false)** +**addDirectories(array $directories, bool $recursive = false)** + +Adds all files from the directory or directories, optionally recursing into sub-directories. Directories are +absolute paths to actual directories. + +**addPath(string $path, bool $recursive = true)** +**addPaths(array $path, bool $recursive = true)** + +Adds all files indicated by the relative paths. Paths are references to actual files or directories relative +to ``$source``. If the relative path resolves to a directory then ``$recursive`` will include sub-directories. + +**addUri(string $uri)** +**addUris(array $uris)** + +Downloads the contents of a URI using ``CURLRequest`` into the scratch workspace then adds the resulting +file to the list. + +.. note:: The CURL request made is a simple ``GET`` and uses the response body for the file contents. Some + remote files may need a custom request to be handled properly. + +Filtering Files +=============== + +**removePattern(string $pattern, string $scope = null)** +**retainPattern(string $pattern, string $scope = null)** + +Filters the current file list through the pattern (and optional scope), removing or retaining matched +files. ``$pattern`` may be a complete regex (like ``'#[A-Za-z]+\.php#'``) or a pseudo-regex similar +to ``glob()`` (like ``*.css``). +If a ``$scope`` is provided then only files in or under that directory will be considered (i.e. files +outside of ``$scope`` are always retained). When no scope is provided then all files are subject. + +Examples:: + + $publisher = new Publisher(APPPATH . 'Config'); + $publisher->addPath('/', true); // Adds all Config files and directories + + $publisher->removePattern('*tion.php'); // Would remove Encryption.php, Validation.php, and boot/production.php + $publisher->removePattern('*tion.php', APPPATH . 'Config/boot'); // Would only remove boot/production.php + + $publisher->retainPattern('#A.+php$#'); // Would keep only Autoload.php + $publisher->retainPattern('#d.+php$#', APPPATH . 'Config/boot'); // Would keep everything but boot/production.php and boot/testing.php + +Outputting Files +================ + +**wipe()** + +Removes all files, directories, and sub-directories from ``$destination``. + +.. important:: Use wisely. + +**copy(bool $replace = true): bool** + +Copies all files into the ``$destination``. This does not recreate the directory structure, so every file +from the current list will end up in the same destination directory. Using ``$replace`` will cause files +to overwrite when there is already an existing file. Returns success or failure, use ``getPublished()`` +and ``getErrors()`` to troubleshoot failures. +Be mindful of duplicate basename collisions, for example:: + + $publisher = new Publisher('/home/source', '/home/destination'); + $publisher->addPaths([ + 'pencil/lead.png', + 'metal/lead.png', + ]); + + // This is bad! Only one file will remain at /home/destination/lead.png + $publisher->copy(true); + +**merge(bool $replace = true): bool** + +Copies all files into the ``$destination`` in appropriate relative sub-directories. Any files that +match ``$source`` will be placed into their equivalent directories in ``$destination``, effectively +creating a "mirror" or "rsync" operation. Using ``$replace`` will cause files +to overwrite when there is already an existing file; since directories are merged this will not +affect other files in the destination. Returns success or failure, use ``getPublished()`` and +``getErrors()`` to troubleshoot failures. + +Example:: + + $publisher = new Publisher('/home/source', '/home/destination'); + $publisher->addPaths([ + 'pencil/lead.png', + 'metal/lead.png', + ]); + + // Results in "/home/destination/pencil/lead.png" and "/home/destination/metal/lead.png" + $publisher->merge(); From 7d4cc0b4820b9f6d45a73de9e45d17d506c33004 Mon Sep 17 00:00:00 2001 From: MGatner Date: Sat, 29 May 2021 15:18:50 +0000 Subject: [PATCH 015/490] Add Publish command --- system/Commands/Utilities/Publish.php | 114 ++++++++++++++++++ system/Language/en/Publisher.php | 5 + system/Publisher/Publisher.php | 1 + tests/_support/Publishers/TestPublisher.php | 29 ++--- tests/system/Commands/PublishCommandTest.php | 52 ++++++++ user_guide_src/source/libraries/publisher.rst | 7 +- 6 files changed, 181 insertions(+), 27 deletions(-) create mode 100644 system/Commands/Utilities/Publish.php create mode 100644 tests/system/Commands/PublishCommandTest.php diff --git a/system/Commands/Utilities/Publish.php b/system/Commands/Utilities/Publish.php new file mode 100644 index 000000000000..b9665286f348 --- /dev/null +++ b/system/Commands/Utilities/Publish.php @@ -0,0 +1,114 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities; + +use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\CLI\CLI; +use CodeIgniter\Publisher\Publisher; + +/** + * Discovers all Publisher classes from the "Publishers/" directory + * across namespaces. Executes `publish()` from each instance, parsing + * each result. + */ +class Publish extends BaseCommand +{ + /** + * The group the command is lumped under + * when listing commands. + * + * @var string + */ + protected $group = 'CodeIgniter'; + + /** + * The Command's name + * + * @var string + */ + protected $name = 'publish'; + + /** + * The Command's short description + * + * @var string + */ + protected $description = 'Discovers and executes all predefined Publisher classes.'; + + /** + * The Command's usage + * + * @var string + */ + protected $usage = 'publish [directory]'; + + /** + * The Command's arguments + * + * @var array + */ + protected $arguments = [ + 'directory' => '[Optional] The directory to scan within each namespace. Default: "Publishers".', + ]; + + /** + * the Command's Options + * + * @var array + */ + protected $options = []; + + //-------------------------------------------------------------------- + + /** + * Displays the help for the spark cli script itself. + * + * @param array $params + */ + public function run(array $params) + { + $directory = array_shift($params) ?? 'Publishers'; + + if ([] === $publishers = Publisher::discover($directory)) + { + CLI::write(lang('Publisher.publishMissing', [$directory])); + return; + } + + foreach ($publishers as $publisher) + { + if ($publisher->publish()) + { + CLI::write(lang('Publisher.publishSuccess', [ + get_class($publisher), + count($publisher->getPublished()), + $publisher->getDestination(), + ]), 'green'); + } + else + { + CLI::error(lang('Publisher.publishFailure', [ + get_class($publisher), + $publisher->getDestination(), + ]), 'light_gray', 'red'); + + foreach ($publisher->getErrors() as $file => $exception) + { + CLI::write($file); + CLI::newLine(); + + $this->showError($exception); + } + } + } + } +} diff --git a/system/Language/en/Publisher.php b/system/Language/en/Publisher.php index cb5ce81f10a4..934342b4f98c 100644 --- a/system/Language/en/Publisher.php +++ b/system/Language/en/Publisher.php @@ -14,4 +14,9 @@ 'collision' => 'Publisher encountered an unexpected {0} while copying {1} to {2}.', 'expectedDirectory' => 'Publisher::{0} expects a valid directory.', 'expectedFile' => 'Publisher::{0} expects a valid file.', + + // Publish Command + 'publishMissing' => 'No Publisher classes detected in {0} across all namespaces.', + 'publishSuccess' => '{0} published {1} file(s) to {2}.', + 'publishFailure' => '{0} failed to publish to {1}!', ]; diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index b9e5cca24e90..7018df15c7d1 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -317,6 +317,7 @@ public function __destruct() */ public function publish(): bool { + // Safeguard against accidental misuse if ($this->source === ROOTPATH && $this->destination === FCPATH) { throw new RuntimeException('Child classes of Publisher should provide their own source and destination or publish method.'); diff --git a/tests/_support/Publishers/TestPublisher.php b/tests/_support/Publishers/TestPublisher.php index 9192c3899b83..cf1eefa7ede6 100644 --- a/tests/_support/Publishers/TestPublisher.php +++ b/tests/_support/Publishers/TestPublisher.php @@ -3,6 +3,7 @@ namespace Tests\Support\Publishers; use CodeIgniter\Publisher\Publisher; +use RuntimeException; final class TestPublisher extends Publisher { @@ -11,17 +12,17 @@ final class TestPublisher extends Publisher * * @return $this */ - public static function setError(string $file) + public static function setResult(bool $result) { - self::$error = $file; + self::$result = $result; } /** - * A file to cause an error + * Return value for publish() * - * @var string + * @var boolean */ - private static $error = ''; + private static $result = true; /** * Base path to use for the source. @@ -42,24 +43,8 @@ public static function setError(string $file) */ public function publish(): bool { - $this->errors = $this->published = []; - $this->addPath(''); - // Copy each sourced file to its relative destination - foreach ($this->getFiles() as $file) - { - if ($file === self::$error) - { - $this->errors[$file] = new RuntimeException('Have an error, dear.'); - } - else - { - // Resolve the destination path - $this->published[] = $this->destination . substr($file, strlen($this->source)); - } - } - - return $this->errors === []; + return self::$result; } } diff --git a/tests/system/Commands/PublishCommandTest.php b/tests/system/Commands/PublishCommandTest.php new file mode 100644 index 000000000000..44f2de25f85a --- /dev/null +++ b/tests/system/Commands/PublishCommandTest.php @@ -0,0 +1,52 @@ +streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); + $this->streamFilter = stream_filter_append(STDERR, 'CITestStreamFilter'); + } + + protected function tearDown(): void + { + parent::tearDown(); + + stream_filter_remove($this->streamFilter); + TestPublisher::setResult(true); + } + + public function testDefault() + { + command('publish'); + + $this->assertStringContainsString(lang('Publisher.publishSuccess', [ + TestPublisher::class, + 0, + WRITEPATH, + ]), CITestStreamFilter::$buffer); + } + + public function testFailure() + { + TestPublisher::setResult(false); + + command('publish'); + + $this->assertStringContainsString(lang('Publisher.publishFailure', [ + TestPublisher::class, + WRITEPATH, + ]), CITestStreamFilter::$buffer); + } +} diff --git a/user_guide_src/source/libraries/publisher.rst b/user_guide_src/source/libraries/publisher.rst index ef97e0b52f95..23f7c2d9e97e 100644 --- a/user_guide_src/source/libraries/publisher.rst +++ b/user_guide_src/source/libraries/publisher.rst @@ -246,7 +246,7 @@ into an application for use:: use CodeIgniter\Publisher\Publisher; use Throwable; - class Publish extends BaseCommand + class AuthPublish extends BaseCommand { protected $group = 'Auth'; protected $name = 'auth:publish'; @@ -275,11 +275,8 @@ into an application for use:: } // If publication succeeded then update namespaces - foreach ($publisher->getFiles as $original) + foreach ($publisher->getPublished() as $file) { - // Get the location of the new file - $file = str_replace($source, APPPATH, $original); - // Replace the namespace $contents = file_get_contents($file); $contents = str_replace('namespace Math\\Auth', 'namespace ' . APP_NAMESPACE, ); From 461942d3eb6fdd7f9e384f16bd4fc0852c176646 Mon Sep 17 00:00:00 2001 From: MGatner Date: Sat, 29 May 2021 15:43:14 +0000 Subject: [PATCH 016/490] Fix test bleeding --- tests/system/Publisher/PublisherSupportTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Publisher/PublisherSupportTest.php b/tests/system/Publisher/PublisherSupportTest.php index f17be3460432..2aff0e34d1f6 100644 --- a/tests/system/Publisher/PublisherSupportTest.php +++ b/tests/system/Publisher/PublisherSupportTest.php @@ -53,7 +53,7 @@ public function testDiscoverNothing() public function testDiscoverStores() { $publisher = Publisher::discover()[0]; - $publisher->addFile($this->file); + $publisher->setFiles([])->addFile($this->file); $result = Publisher::discover(); $this->assertSame($publisher, $result[0]); From 746ed2c7ce7fc487d31f4959e5b90dfb1b1adbb0 Mon Sep 17 00:00:00 2001 From: MGatner Date: Sat, 29 May 2021 20:17:28 +0000 Subject: [PATCH 017/490] Fix UG format --- user_guide_src/source/libraries/publisher.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/publisher.rst b/user_guide_src/source/libraries/publisher.rst index 23f7c2d9e97e..c30bce720815 100644 --- a/user_guide_src/source/libraries/publisher.rst +++ b/user_guide_src/source/libraries/publisher.rst @@ -119,6 +119,8 @@ You want to display a "photo of the day" image on your homepage. You have a feed need to get the actual file into a browsable location in your project at **public/images/daily_photo.jpg**. You can set up :doc:`Custom Command ` to run daily that will handle this for you:: + composer require twbs/bootstrap > php spark publish -... and you'll end up with something like this: +... and you'll end up with something like this:: public/.htaccess public/favicon.ico @@ -240,6 +244,8 @@ You want to allow developers using your popular authentication module the abilit of your Migration, Controller, and Model. You can create your own module "publish" command to inject these components into an application for use:: + Date: Tue, 1 Jun 2021 14:44:25 +0000 Subject: [PATCH 018/490] Implement review changes --- system/Commands/Utilities/Publish.php | 5 ++--- .../Exceptions/PublisherException.php | 21 +++++++++++++++++++ tests/_support/Publishers/TestPublisher.php | 2 -- user_guide_src/source/libraries/index.rst | 1 + user_guide_src/source/libraries/publisher.rst | 18 +++++++++------- 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/system/Commands/Utilities/Publish.php b/system/Commands/Utilities/Publish.php index b9665286f348..15448fc90db3 100644 --- a/system/Commands/Utilities/Publish.php +++ b/system/Commands/Utilities/Publish.php @@ -49,7 +49,7 @@ class Publish extends BaseCommand * * @var string */ - protected $usage = 'publish [directory]'; + protected $usage = 'publish []'; /** * The Command's arguments @@ -104,9 +104,8 @@ public function run(array $params) foreach ($publisher->getErrors() as $file => $exception) { CLI::write($file); + CLI::error($exception->getMessage()); CLI::newLine(); - - $this->showError($exception); } } } diff --git a/system/Publisher/Exceptions/PublisherException.php b/system/Publisher/Exceptions/PublisherException.php index 81f9bb4b7174..8144fcfeac02 100644 --- a/system/Publisher/Exceptions/PublisherException.php +++ b/system/Publisher/Exceptions/PublisherException.php @@ -13,18 +13,39 @@ use CodeIgniter\Exceptions\FrameworkException; +/** + * Publisher Exception Class + * + * Handles exceptions related to actions taken by a Publisher. + */ class PublisherException extends FrameworkException { + /** + * Throws when a file should be overwritten yet cannot. + * + * @param string $from The source file + * @param string $to The destination file + */ public static function forCollision(string $from, string $to) { return new static(lang('Publisher.collision', [filetype($to), $from, $to])); } + /** + * Throws when an object is expected to be a directory but is not or is missing. + * + * @param string $caller The method causing the exception + */ public static function forExpectedDirectory(string $caller) { return new static(lang('Publisher.expectedDirectory', [$caller])); } + /** + * Throws when an object is expected to be a file but is not or is missing. + * + * @param string $caller The method causing the exception + */ public static function forExpectedFile(string $caller) { return new static(lang('Publisher.expectedFile', [$caller])); diff --git a/tests/_support/Publishers/TestPublisher.php b/tests/_support/Publishers/TestPublisher.php index cf1eefa7ede6..47c987a0285a 100644 --- a/tests/_support/Publishers/TestPublisher.php +++ b/tests/_support/Publishers/TestPublisher.php @@ -9,8 +9,6 @@ final class TestPublisher extends Publisher { /** * Fakes an error on the given file. - * - * @return $this */ public static function setResult(bool $result) { diff --git a/user_guide_src/source/libraries/index.rst b/user_guide_src/source/libraries/index.rst index 3af27e900ce5..9f977e91d604 100644 --- a/user_guide_src/source/libraries/index.rst +++ b/user_guide_src/source/libraries/index.rst @@ -14,6 +14,7 @@ Library Reference honeypot images pagination + publisher security sessions throttler diff --git a/user_guide_src/source/libraries/publisher.rst b/user_guide_src/source/libraries/publisher.rst index c30bce720815..57abcfcd6925 100644 --- a/user_guide_src/source/libraries/publisher.rst +++ b/user_guide_src/source/libraries/publisher.rst @@ -44,11 +44,12 @@ By default the source and destination will be set to ``ROOTPATH`` and ``FCPATH`` easy access to take any file from your project and make it web-accessible. Alternatively you may pass a new source or source and destination into the constructor:: + use CodeIgniter\Publisher\Publisher; + $vendorPublisher = new Publisher(ROOTPATH . 'vendor'); $filterPublisher = new Publisher('/path/to/module/Filters', APPPATH . 'Filters'); -Once the source and destination are set you may start adding relative input files:: - + // Once the source and destination are set you may start adding relative input files $frameworkPublisher = new Publisher(ROOTPATH . 'vendor/codeigniter4/codeigniter4'); // All "path" commands are relative to $source @@ -73,7 +74,7 @@ to their destination(s):: // Place files into their relative $destination directories, overwriting and saving the boolean result $result = $frameworkPublisher->merge(true); -See the Library Reference for a full description of available methods. +See the :ref:`reference` for a full description of available methods. Automation and Discovery ======================== @@ -90,7 +91,7 @@ the powerful ``Autoloader`` to locate any child classes primed for publication:: if ($result === false) { - CLI::write(get_class($publisher) . ' failed to publish!', 'red'); + CLI::error(get_class($publisher) . ' failed to publish!', 'red'); } } @@ -139,7 +140,7 @@ You can set up :doc:`Custom Command ` to run daily that will try { - $publisher->addPath('daily_photo.jpg')->copy($replace = true); + $publisher->addPath('daily_photo.jpg')->copy(true); // `true` to enable overwrites } catch (Throwable $e) { @@ -152,7 +153,7 @@ Now running ``spark publish:daily`` will keep your homepage's image up-to-date. coming from an external API? You can use ``addUri()`` in place of ``addPath()`` to download the remote resource and publish it out instead:: - $publisher->addUri('https://example.com/feeds/daily_photo.jpg')->copy($replace = true); + $publisher->addUri('https://example.com/feeds/daily_photo.jpg')->copy(true); Asset Dependencies Example ========================== @@ -161,8 +162,6 @@ You want to integrate the frontend library "Bootstrap" into your project, but th to keep up with. You can create a publication definition in your project to sync frontend assets by adding extending ``Publisher`` in your project. So **app/Publishers/BootstrapPublisher.php** might look like this:: - merge(true); } + } Now add the dependency via Composer and call ``spark publish`` to run the publication:: @@ -298,6 +298,8 @@ Now when your module users run ``php spark auth:publish`` they will have the fol app/Models/LoginModel.php app/Models/UserModel.php +.. _reference: + ***************** Library Reference ***************** From 6484cb6304e141e0c8f420ec3866a9212f711aec Mon Sep 17 00:00:00 2001 From: MGatner Date: Tue, 1 Jun 2021 17:23:22 +0000 Subject: [PATCH 019/490] Tweak formatting --- user_guide_src/source/libraries/publisher.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/libraries/publisher.rst b/user_guide_src/source/libraries/publisher.rst index 57abcfcd6925..1cb27b54aa2b 100644 --- a/user_guide_src/source/libraries/publisher.rst +++ b/user_guide_src/source/libraries/publisher.rst @@ -162,6 +162,8 @@ You want to integrate the frontend library "Bootstrap" into your project, but th to keep up with. You can create a publication definition in your project to sync frontend assets by adding extending ``Publisher`` in your project. So **app/Publishers/BootstrapPublisher.php** might look like this:: + addPath('dist') // Indicate we only want the minimized versions - ->retainPattern('*.min.*) + ->retainPattern('*.min.*') // Merge-and-replace to retain the original directory structure ->merge(true); From 55e79f6ef4a2181608fc57ce8b346645cf565b3e Mon Sep 17 00:00:00 2001 From: MGatner Date: Tue, 8 Jun 2021 16:22:12 +0000 Subject: [PATCH 020/490] Add Publisher restrictions --- app/Config/Publisher.php | 28 ++++ system/Config/Publisher.php | 42 ++++++ system/Language/en/Publisher.php | 8 +- .../Exceptions/PublisherException.php | 22 +++ system/Publisher/Publisher.php | 131 +++++++++++------- tests/_support/Config/Registrar.php | 14 ++ tests/system/Publisher/PublisherInputTest.php | 2 +- .../system/Publisher/PublisherOutputTest.php | 5 +- .../Publisher/PublisherRestrictionsTest.php | 108 +++++++++++++++ .../system/Publisher/PublisherSupportTest.php | 3 +- user_guide_src/source/libraries/publisher.rst | 16 ++- 11 files changed, 325 insertions(+), 54 deletions(-) create mode 100644 app/Config/Publisher.php create mode 100644 system/Config/Publisher.php create mode 100644 tests/system/Publisher/PublisherRestrictionsTest.php diff --git a/app/Config/Publisher.php b/app/Config/Publisher.php new file mode 100644 index 000000000000..2588eea2abac --- /dev/null +++ b/app/Config/Publisher.php @@ -0,0 +1,28 @@ + + */ + public $restrictions = [ + ROOTPATH => '*', + FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', + ]; +} diff --git a/system/Config/Publisher.php b/system/Config/Publisher.php new file mode 100644 index 000000000000..651600ef9c06 --- /dev/null +++ b/system/Config/Publisher.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeIgniter\Config; + +/** + * Publisher Configuration + * + * Defines basic security restrictions for the Publisher class + * to prevent abuse by injecting malicious files into a project. + */ +class Publisher extends BaseConfig +{ + /** + * A list of allowed destinations with a (pseudo-)regex + * of allowed files for each destination. + * Attempts to publish to directories not in this list will + * result in a PublisherException. Files that do no fit the + * pattern will cause copy/merge to fail. + * + * @var array + */ + public $restrictions = [ + ROOTPATH => '*', + FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', + ]; + + /** + * Disables Registrars to prevent modules from altering the restrictions. + */ + final protected function registerProperties() + { + } +} diff --git a/system/Language/en/Publisher.php b/system/Language/en/Publisher.php index 934342b4f98c..2d7ae8418b25 100644 --- a/system/Language/en/Publisher.php +++ b/system/Language/en/Publisher.php @@ -11,9 +11,11 @@ // Publisher language settings return [ - 'collision' => 'Publisher encountered an unexpected {0} while copying {1} to {2}.', - 'expectedDirectory' => 'Publisher::{0} expects a valid directory.', - 'expectedFile' => 'Publisher::{0} expects a valid file.', + 'collision' => 'Publisher encountered an unexpected {0} while copying {1} to {2}.', + 'expectedDirectory' => 'Publisher::{0} expects a valid directory.', + 'expectedFile' => 'Publisher::{0} expects a valid file.', + 'destinationNotAllowed' => 'Destination is not on the allowed list of Publisher directories: {0}', + 'fileNotAllowed' => '{0} fails the following restriction for {1}: {2}', // Publish Command 'publishMissing' => 'No Publisher classes detected in {0} across all namespaces.', diff --git a/system/Publisher/Exceptions/PublisherException.php b/system/Publisher/Exceptions/PublisherException.php index 8144fcfeac02..d54420881ec7 100644 --- a/system/Publisher/Exceptions/PublisherException.php +++ b/system/Publisher/Exceptions/PublisherException.php @@ -50,4 +50,26 @@ public static function forExpectedFile(string $caller) { return new static(lang('Publisher.expectedFile', [$caller])); } + + /** + * Throws when given a destination that is not in the list of allowed directories. + * + * @param string $destination + */ + public static function forDestinationNotAllowed(string $destination) + { + return new static(lang('Publisher.destinationNotAllowed', [$destination])); + } + + /** + * Throws when a file fails to match the allowed pattern for its destination. + * + * @param string $file + * @param string $directory + * @param string $pattern + */ + public static function forFileNotAllowed(string $file, string $directory, string $pattern) + { + return new static(lang('Publisher.fileNotAllowed', [$file, $directory, $pattern])); + } } diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index 7018df15c7d1..1bfd4b91768d 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -72,6 +72,14 @@ class Publisher */ private $published = []; + /** + * List of allowed directories and their allowed files regex. + * Restrictions are intentionally private to prevent overriding. + * + * @var array + */ + private $restrictions; + /** * Base path to use for the source. * @@ -134,6 +142,8 @@ final public static function discover(string $directory = 'Publishers'): array * @param string $directory * * @return string + * + * @throws PublisherException */ private static function resolveDirectory(string $directory): string { @@ -152,6 +162,8 @@ private static function resolveDirectory(string $directory): string * @param string $file * * @return string + * + * @throws PublisherException */ private static function resolveFile(string $file): string { @@ -191,7 +203,7 @@ private static function filterFiles(array $files, string $directory): array * * @return string[] */ - private static function matchFiles(array $files, string $pattern) + private static function matchFiles(array $files, string $pattern): array { // Convert pseudo-regex into their true form if (@preg_match($pattern, null) === false) // @phpstan-ignore-line @@ -236,49 +248,6 @@ private static function wipeDirectory(string $directory): void } } - /** - * Copies a file with directory creation and identical file awareness. - * Intentionally allows errors. - * - * @param string $from - * @param string $to - * @param boolean $replace - * - * @return void - * - * @throws PublisherException For unresolvable collisions - */ - private static function safeCopyFile(string $from, string $to, bool $replace): void - { - // Check for an existing file - if (file_exists($to)) - { - // If not replacing or if files are identical then consider successful - if (! $replace || same_file($from, $to)) - { - return; - } - - // If it is a directory then do not try to remove it - if (is_dir($to)) - { - throw PublisherException::forCollision($from, $to); - } - - // Try to remove anything else - unlink($to); - } - - // Make sure the directory exists - if (! is_dir($directory = pathinfo($to, PATHINFO_DIRNAME))) - { - mkdir($directory, 0775, true); - } - - // Allow copy() to throw errors - copy($from, $to); - } - //-------------------------------------------------------------------- /** @@ -293,6 +262,20 @@ public function __construct(string $source = null, string $destination = null) $this->source = self::resolveDirectory($source ?? $this->source); $this->destination = self::resolveDirectory($destination ?? $this->destination); + + // Restrictions are intentionally not injected to prevent overriding + $this->restrictions = config('Publisher')->restrictions; + + // Make sure the destination is allowed + foreach (array_keys($this->restrictions) as $directory) + { + if (strpos($this->destination, $directory) === 0) + { + return; + } + } + + throw PublisherException::forDestinationNotAllowed($this->destination); } /** @@ -314,6 +297,8 @@ public function __destruct() * discovery. * * @return boolean + * + * @throws RuntimeException */ public function publish(): bool { @@ -683,7 +668,7 @@ final public function copy(bool $replace = true): bool try { - self::safeCopyFile($file, $to, $replace); + $this->safeCopyFile($file, $to, $replace); $this->published[] = $to; } catch (Throwable $e) @@ -707,7 +692,7 @@ final public function merge(bool $replace = true): bool { $this->errors = $this->published = []; - // Get the file from source for special handling + // Get the files from source for special handling $sourced = self::filterFiles($this->getFiles(), $this->source); // Handle everything else with a flat copy @@ -722,7 +707,7 @@ final public function merge(bool $replace = true): bool try { - self::safeCopyFile($file, $to, $replace); + $this->safeCopyFile($file, $to, $replace); $this->published[] = $to; } catch (Throwable $e) @@ -733,4 +718,56 @@ final public function merge(bool $replace = true): bool return $this->errors === []; } + + /** + * Copies a file with directory creation and identical file awareness. + * Intentionally allows errors. + * + * @param string $from + * @param string $to + * @param boolean $replace + * + * @return void + * + * @throws PublisherException For collisions and restriction violations + */ + private function safeCopyFile(string $from, string $to, bool $replace): void + { + // Verify this is an allowed file for its destination + foreach ($this->restrictions as $directory => $pattern) + { + if (strpos($to, $directory) === 0 && self::matchFiles([$to], $pattern) === []) + { + throw PublisherException::forFileNotAllowed($from, $directory, $pattern); + } + } + + // Check for an existing file + if (file_exists($to)) + { + // If not replacing or if files are identical then consider successful + if (! $replace || same_file($from, $to)) + { + return; + } + + // If it is a directory then do not try to remove it + if (is_dir($to)) + { + throw PublisherException::forCollision($from, $to); + } + + // Try to remove anything else + unlink($to); + } + + // Make sure the directory exists + if (! is_dir($directory = pathinfo($to, PATHINFO_DIRNAME))) + { + mkdir($directory, 0775, true); + } + + // Allow copy() to throw errors + copy($from, $to); + } } diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php index 4a9f3b0fe981..15e8f54ff92d 100644 --- a/tests/_support/Config/Registrar.php +++ b/tests/_support/Config/Registrar.php @@ -121,4 +121,18 @@ public static function Database() return $config; } + + /** + * Demonstrates Publisher security. + * + * @see PublisherRestrictionsTest::testRegistrarsNotAllowed() + * + * @return array + */ + public static function Publisher() + { + return [ + 'restrictions' => [SUPPORTPATH => '*'], + ]; + } } diff --git a/tests/system/Publisher/PublisherInputTest.php b/tests/system/Publisher/PublisherInputTest.php index 553a92b14c53..e7450f5c02a9 100644 --- a/tests/system/Publisher/PublisherInputTest.php +++ b/tests/system/Publisher/PublisherInputTest.php @@ -1,4 +1,4 @@ -root = vfsStream::setup('root', null, $this->structure); + + // Add root to the list of allowed destinations + config('Publisher')->restrictions[$this->root->url()] = '*'; } //-------------------------------------------------------------------- diff --git a/tests/system/Publisher/PublisherRestrictionsTest.php b/tests/system/Publisher/PublisherRestrictionsTest.php new file mode 100644 index 000000000000..27db2e9429a9 --- /dev/null +++ b/tests/system/Publisher/PublisherRestrictionsTest.php @@ -0,0 +1,108 @@ +assertArrayNotHasKey(SUPPORTPATH, config('Publisher')->restrictions); + } + + public function testImmutableRestrictions() + { + $publisher = new Publisher(); + + // Try to "hack" the Publisher by adding our desired destination to the config + config('Publisher')->restrictions[SUPPORTPATH] = '*'; + + $restrictions = $this->getPrivateProperty($publisher, 'restrictions'); + + $this->assertArrayNotHasKey(SUPPORTPATH, $restrictions); + } + + /** + * @dataProvider fileProvider + */ + public function testDefaultPublicRestrictions(string $path) + { + $publisher = new Publisher(ROOTPATH, FCPATH); + $pattern = config('Publisher')->restrictions[FCPATH]; + + // Use the scratch space to create a file + $file = $publisher->getScratch() . $path; + file_put_contents($file, 'To infinity and beyond!'); + + $result = $publisher->addFile($file)->merge(); + $this->assertFalse($result); + + $errors = $publisher->getErrors(); + $this->assertCount(1, $errors); + $this->assertSame([$file], array_keys($errors)); + + $expected = lang('Publisher.fileNotAllowed', [$file, FCPATH, $pattern]); + $this->assertSame($expected, $errors[$file]->getMessage()); + } + + public function fileProvider() + { + yield 'php' => ['index.php']; + yield 'exe' => ['cat.exe']; + yield 'flat' => ['banana']; + } + + /** + * @dataProvider destinationProvider + */ + public function testDestinations(string $destination, bool $allowed) + { + config('Publisher')->restrictions = [ + APPPATH => '', + FCPATH => '', + SUPPORTPATH . 'Files' => '', + SUPPORTPATH . 'Files/../' => '', + ]; + + if (! $allowed) + { + $this->expectException(PublisherException::class); + $this->expectExceptionMessage(lang('Publisher.destinationNotAllowed', [$destination])); + } + + $publisher = new Publisher(null, $destination); + $this->assertInstanceOf(Publisher::class, $publisher); + } + + public function destinationProvider() + { + return [ + 'explicit' => [ + APPPATH, + true, + ], + 'subdirectory' => [ + APPPATH . 'Config', + true, + ], + 'relative' => [ + SUPPORTPATH . 'Files/able/../', + true, + ], + 'parent' => [ + SUPPORTPATH, + false, + ], + ]; + } +} diff --git a/tests/system/Publisher/PublisherSupportTest.php b/tests/system/Publisher/PublisherSupportTest.php index 2aff0e34d1f6..42f22d721525 100644 --- a/tests/system/Publisher/PublisherSupportTest.php +++ b/tests/system/Publisher/PublisherSupportTest.php @@ -1,4 +1,4 @@ -assertDirectoryExists($directory); + config('Publisher')->restrictions[$directory] = ''; // Allow the directory $publisher = new Publisher($this->directory, $directory); $publisher->wipe(); diff --git a/user_guide_src/source/libraries/publisher.rst b/user_guide_src/source/libraries/publisher.rst index 1cb27b54aa2b..84b430ba144b 100644 --- a/user_guide_src/source/libraries/publisher.rst +++ b/user_guide_src/source/libraries/publisher.rst @@ -107,6 +107,20 @@ Most of the time you will not need to handle your own discovery, just use the pr By default on your class extension ``publish()`` will add all files from your ``$source`` and merge them out to your destination, overwriting on collision. +Security +======== + +In order to prevent modules from injecting malicious code into your projects, ``Publisher`` contains a config file +that defines which directories and file patterns are allowed as destinations. By default, files may only be published +to your project (to prevent access to the rest of the filesystem), and the **public/** folder (``FCPATH``) will only +receive files with the following extensions: +* Web assets: css, scss, js, map +* Non-executable web files: htm, html, xml, json, webmanifest +* Fonts: tff, eot, woff +* Images: gif, jpg, jpeg, tiff, png, webp, bmp, ico, svg + +If you need to add or adjust the security for your project then alter the ``$restrictions`` property of ``Config\Publisher``. + ******** Examples ******** @@ -159,7 +173,7 @@ Asset Dependencies Example ========================== You want to integrate the frontend library "Bootstrap" into your project, but the frequent updates makes it a hassle -to keep up with. You can create a publication definition in your project to sync frontend assets by adding extending +to keep up with. You can create a publication definition in your project to sync frontend assets by extending ``Publisher`` in your project. So **app/Publishers/BootstrapPublisher.php** might look like this:: Date: Sat, 12 Jun 2021 13:25:40 +0200 Subject: [PATCH 021/490] Graphic fix on some screen res Before: https://prnt.sc/1559re2 After: https://prnt.sc/1559uou --- app/Views/welcome_message.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Views/welcome_message.php b/app/Views/welcome_message.php index 7050aa91174a..9ee2e427c308 100644 --- a/app/Views/welcome_message.php +++ b/app/Views/welcome_message.php @@ -163,7 +163,7 @@ color: rgba(200, 200, 200, 1); padding: .25rem 1.75rem; } - @media (max-width: 559px) { + @media (max-width: 629px) { header ul { padding: 0; } From 0b1470eaf86ba6c416ff766973bd62f7004fbdda Mon Sep 17 00:00:00 2001 From: Toto Prayogo Date: Tue, 15 Jun 2021 11:30:43 +0700 Subject: [PATCH 022/490] rename `application` to `app` --- user_guide_src/source/libraries/sessions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/sessions.rst b/user_guide_src/source/libraries/sessions.rst index ac4a20719b8c..1472afa4000c 100644 --- a/user_guide_src/source/libraries/sessions.rst +++ b/user_guide_src/source/libraries/sessions.rst @@ -610,7 +610,7 @@ setting**. The examples below work both on MySQL and PostgreSQL:: ALTER TABLE ci_sessions DROP PRIMARY KEY; You can choose the Database group to use by adding a new line to the -**application\Config\App.php** file with the name of the group to use:: +**app/Config/App.php** file with the name of the group to use:: public $sessionDBGroup = 'groupName'; From 9dbb7d8471fb106d35a756dc328c49b4897173fd Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Tue, 22 Jun 2021 00:27:38 +0800 Subject: [PATCH 023/490] Make CLI detection as interface independent as possible --- system/Common.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/system/Common.php b/system/Common.php index 19be6398a8e1..4dfcd2cee170 100644 --- a/system/Common.php +++ b/system/Common.php @@ -644,22 +644,14 @@ function helper($filenames) /** * Check if PHP was invoked from the command line. * - * @codeCoverageIgnore Cannot be tested fully as PHPUnit always run in CLI + * @codeCoverageIgnore Cannot be tested fully as PHPUnit always run in php-cli */ function is_cli(): bool { - if (PHP_SAPI === 'cli') { - return true; - } - if (defined('STDIN')) { return true; } - if (stristr(PHP_SAPI, 'cgi') && getenv('TERM')) { - return true; - } - if (! isset($_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT']) && isset($_SERVER['argv']) && count($_SERVER['argv']) > 0) { return true; } From 6a5b2f6c2a315e4e0ba2f4112cd7e0424ee73164 Mon Sep 17 00:00:00 2001 From: sclubricants Date: Mon, 21 Jun 2021 09:31:14 -0700 Subject: [PATCH 024/490] Added MySQLi resultMode and updated user guide --- system/Database/MySQLi/Connection.php | 15 ++++++++++- user_guide_src/source/database/results.rst | 30 ++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 756057528450..78652c25b3f3 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -58,6 +58,19 @@ class Connection extends BaseConnection */ public $mysqli; + /** + * MySQLi constant + * + * For unbuffered queries use `MYSQLI_USE_RESULT`. + * + * Default mode for buffered queries uses `MYSQLI_STORE_RESULT`. + * + * @var int + */ + public $resultMode = MYSQLI_STORE_RESULT; + + //-------------------------------------------------------------------- + /** * Connect to the database. * @@ -277,7 +290,7 @@ protected function execute(string $sql) } try { - return $this->connID->query($this->prepQuery($sql)); + return $this->connID->query($this->prepQuery($sql), $this->resultMode); } catch (mysqli_sql_exception $e) { log_message('error', $e->getMessage()); diff --git a/user_guide_src/source/database/results.rst b/user_guide_src/source/database/results.rst index e3e573d5c460..018c5184cc1b 100644 --- a/user_guide_src/source/database/results.rst +++ b/user_guide_src/source/database/results.rst @@ -159,6 +159,36 @@ it returns the current row and moves the internal data pointer ahead. echo $row->body; } +For use with MySQLi you may set MySQLi's result mode to +``MYSQLI_USE_RESULT`` for maximum memory savings. Use of this is not +generally recommended but it can be beneficial in some circumstances +such as writing large queries to csv. If you change the result mode +be aware of the tradeoffs associated with it. + +:: + + $db->resultMode = MYSQLI_USE_RESULT; // for unbuffered results + + $query = $db->query("YOUR QUERY"); + + $file = new \CodeIgniter\Files\File(WRITEPATH.'data.csv'); + + $csv = $file->openFile('w'); + + while ($row = $query->getUnbufferedRow('array')) + { + $csv->fputcsv($row); + } + + $db->resultMode = MYSQLI_STORE_RESULT; // return to default mode + +.. note:: When using ``MYSQLI_USE_RESULT`` all subsequent calls on the same + connection will result in error until all records have been fetched or + a ``freeResult()`` call has been made. The ``getNumRows()`` method will only + return the number of rows based on the current position of the data pointer. + MyISAM tables will remain locked until all the records have been fetched + or a ``freeResult()`` call has been made. + You can optionally pass 'object' (default) or 'array' in order to specify the returned value's type:: From b0c76eda8f55f44aee8a2444a322f93c5f12d29a Mon Sep 17 00:00:00 2001 From: Alex Schmitz Date: Wed, 23 Jun 2021 17:57:44 +0200 Subject: [PATCH 025/490] Sort timeline elements by start and duration. --- system/Debug/Toolbar.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 16432054d2e2..32e1f951a3df 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -213,6 +213,12 @@ protected function collectTimelineData($collectors): array } // Sort it + $sortArray = [ + array_column($data, 'start'), SORT_NUMERIC, SORT_ASC, + array_column($data, 'duration'), SORT_NUMERIC, SORT_DESC, + &$data + ]; + array_multisort(...$sortArray); return $data; } From f00ced2d1990e3429d1fd6a5ef2d7b2281d89ed0 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 24 Jun 2021 21:57:34 +0800 Subject: [PATCH 026/490] Namespace extracted by GeneratorTrait should always end in backslash --- system/CLI/GeneratorTrait.php | 6 +++--- .../system/Commands/CommandGeneratorTest.php | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/system/CLI/GeneratorTrait.php b/system/CLI/GeneratorTrait.php index b0668a49f4d2..51706b477cd4 100644 --- a/system/CLI/GeneratorTrait.php +++ b/system/CLI/GeneratorTrait.php @@ -215,14 +215,14 @@ protected function qualifyClassName(): string // Trims input, normalize separators, and ensure that all paths are in Pascalcase. $class = ltrim(implode('\\', array_map('pascalize', explode('\\', str_replace('/', '\\', trim($class))))), '\\/'); - // Gets the namespace from input. - $namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\'); + // Gets the namespace from input. Don't forget the ending backslash! + $namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\') . '\\'; if (strncmp($class, $namespace, strlen($namespace)) === 0) { return $class; // @codeCoverageIgnore } - return $namespace . '\\' . $this->directory . '\\' . str_replace('/', '\\', $class); + return $namespace . $this->directory . '\\' . str_replace('/', '\\', $class); } /** diff --git a/tests/system/Commands/CommandGeneratorTest.php b/tests/system/Commands/CommandGeneratorTest.php index abe3ce4c7e6e..1d5dd6d116c4 100644 --- a/tests/system/Commands/CommandGeneratorTest.php +++ b/tests/system/Commands/CommandGeneratorTest.php @@ -132,4 +132,24 @@ public function testGeneratorPreservesCaseButChangesComponentName(): void $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer); $this->assertFileExists(APPPATH . 'Controllers/TestModuleController.php'); } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/4857 + */ + public function testGeneratorIsNotConfusedWithNamespaceLikeClassNames(): void + { + $time = time(); + $notExists = true; + command('make:migration App_Lesson'); + + // we got 5 chances to prove that the file created went to app/Database/Migrations + foreach (range(0, 4) as $increment) { + $expectedFile = sprintf('%sDatabase/Migrations/%s_AppLesson.php', APPPATH, gmdate('Y-m-d-His', $time + $increment)); + clearstatcache(true, $expectedFile); + + $notExists = $notExists && ! is_file($expectedFile); + } + + $this->assertFalse($notExists, 'Creating migration file for class "AppLesson" did not go to "app/Database/Migrations"'); + } } From b12cfd5b16820d795fe32604d4596394285e68e3 Mon Sep 17 00:00:00 2001 From: michalsn Date: Fri, 25 Jun 2021 19:10:40 +0200 Subject: [PATCH 027/490] Fix bug with respecting TTL in Predis and File cache --- system/Cache/Handlers/FileHandler.php | 2 +- system/Cache/Handlers/PredisHandler.php | 4 +++- tests/system/Cache/Handlers/FileHandlerTest.php | 12 ++++++++++++ tests/system/Cache/Handlers/MemcachedHandlerTest.php | 12 ++++++++++++ tests/system/Cache/Handlers/PredisHandlerTest.php | 12 ++++++++++++ tests/system/Cache/Handlers/RedisHandlerTest.php | 12 ++++++++++++ 6 files changed, 52 insertions(+), 2 deletions(-) diff --git a/system/Cache/Handlers/FileHandler.php b/system/Cache/Handlers/FileHandler.php index db2e5d6574ee..b6b1cb9c3525 100644 --- a/system/Cache/Handlers/FileHandler.php +++ b/system/Cache/Handlers/FileHandler.php @@ -210,7 +210,7 @@ public function getMetaData(string $key) } return [ - 'expire' => $data['time'] + $data['ttl'], + 'expire' => $data['ttl'] > 0 ? $data['time'] + $data['ttl'] : null, 'mtime' => filemtime($this->path . $key), 'data' => $data['data'], ]; diff --git a/system/Cache/Handlers/PredisHandler.php b/system/Cache/Handlers/PredisHandler.php index 374b3895ed05..5d1f37cdfc24 100644 --- a/system/Cache/Handlers/PredisHandler.php +++ b/system/Cache/Handlers/PredisHandler.php @@ -127,7 +127,9 @@ public function save(string $key, $value, int $ttl = 60) return false; } - $this->redis->expireat($key, time() + $ttl); + if ($ttl) { + $this->redis->expireat($key, time() + $ttl); + } return true; } diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php index bfd379ae9d8f..0f9f9a10eadf 100644 --- a/tests/system/Cache/Handlers/FileHandlerTest.php +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -156,6 +156,18 @@ public function testSaveExcessiveKeyLength() unlink($file); } + public function testSavePermanent() + { + $this->assertTrue($this->fileHandler->save(self::$key1, 'value', 0)); + $metaData = $this->fileHandler->getMetaData(self::$key1); + + $this->assertSame(null, $metaData['expire']); + $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); + $this->assertSame('value', $metaData['data']); + + $this->assertTrue($this->fileHandler->delete(self::$key1)); + } + public function testDelete() { $this->fileHandler->save(self::$key1, 'value'); diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index f83708bd8c98..9afbdf742236 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -102,6 +102,18 @@ public function testSave() $this->assertTrue($this->memcachedHandler->save(self::$key1, 'value')); } + public function testSavePermanent() + { + $this->assertTrue($this->memcachedHandler->save(self::$key1, 'value', 0)); + $metaData = $this->memcachedHandler->getMetaData(self::$key1); + + $this->assertSame(null, $metaData['expire']); + $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); + $this->assertSame('value', $metaData['data']); + + $this->assertTrue($this->memcachedHandler->delete(self::$key1)); + } + public function testDelete() { $this->memcachedHandler->save(self::$key1, 'value'); diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php index df6dfdbfbaef..867babcddc2a 100644 --- a/tests/system/Cache/Handlers/PredisHandlerTest.php +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php @@ -109,6 +109,18 @@ public function testSave() $this->assertTrue($this->PredisHandler->save(self::$key1, 'value')); } + public function testSavePermanent() + { + $this->assertTrue($this->PredisHandler->save(self::$key1, 'value', 0)); + $metaData = $this->PredisHandler->getMetaData(self::$key1); + + $this->assertSame(null, $metaData['expire']); + $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); + $this->assertSame('value', $metaData['data']); + + $this->assertTrue($this->PredisHandler->delete(self::$key1)); + } + public function testDelete() { $this->PredisHandler->save(self::$key1, 'value'); diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php index 66f3c7fd2244..b3f8cebbb422 100644 --- a/tests/system/Cache/Handlers/RedisHandlerTest.php +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -109,6 +109,18 @@ public function testSave() $this->assertTrue($this->redisHandler->save(self::$key1, 'value')); } + public function testSavePermanent() + { + $this->assertTrue($this->redisHandler->save(self::$key1, 'value', 0)); + $metaData = $this->redisHandler->getMetaData(self::$key1); + + $this->assertSame(null, $metaData['expire']); + $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); + $this->assertSame('value', $metaData['data']); + + $this->assertTrue($this->redisHandler->delete(self::$key1)); + } + public function testDelete() { $this->redisHandler->save(self::$key1, 'value'); From 788a3325fe914298b02dd98acc68a11f9751ce68 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sat, 26 Jun 2021 11:00:32 +0200 Subject: [PATCH 028/490] Fix database session bug with timestamp in MySQLi --- system/Session/Handlers/DatabaseHandler.php | 9 +- .../20160428212500_Create_test_tables.php | 29 ++++ .../_support/Database/Seeds/CITestSeeder.php | 18 +++ tests/system/Database/Live/MetadataTest.php | 4 + .../Session/Handlers/DatabaseHandlerTest.php | 141 ++++++++++++++++++ 5 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 tests/system/Session/Handlers/DatabaseHandlerTest.php diff --git a/system/Session/Handlers/DatabaseHandler.php b/system/Session/Handlers/DatabaseHandler.php index 18da544d2873..ebfdfc119d5e 100644 --- a/system/Session/Handlers/DatabaseHandler.php +++ b/system/Session/Handlers/DatabaseHandler.php @@ -172,11 +172,10 @@ public function write($id, $data): bool $insertData = [ 'id' => $id, 'ip_address' => $this->ipAddress, - 'timestamp' => 'now()', 'data' => $this->platform === 'postgre' ? '\x' . bin2hex($data) : $data, ]; - if (! $this->db->table($this->table)->insert($insertData)) { + if (! $this->db->table($this->table)->set('timestamp', 'now()', false)->insert($insertData)) { return $this->fail(); } @@ -192,13 +191,13 @@ public function write($id, $data): bool $builder = $builder->where('ip_address', $this->ipAddress); } - $updateData = ['timestamp' => 'now()']; + $updateData = []; if ($this->fingerprint !== md5($data)) { $updateData['data'] = ($this->platform === 'postgre') ? '\x' . bin2hex($data) : $data; } - if (! $builder->update($updateData)) { + if (! $builder->set('timestamp', 'now()', false)->update($updateData)) { return $this->fail(); } @@ -257,7 +256,7 @@ public function gc($max_lifetime) $separator = $this->platform === 'postgre' ? '\'' : ' '; $interval = implode($separator, ['', "{$max_lifetime} second", '']); - return $this->db->table($this->table)->delete("timestamp < now() - INTERVAL {$interval}") ? 1 : $this->fail(); + return $this->db->table($this->table)->where('timestamp <', "now() - INTERVAL {$interval}", false)->delete() ? true : $this->fail(); } /** diff --git a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php index 07b61cdd2451..5b414de228a7 100644 --- a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php +++ b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php @@ -127,6 +127,31 @@ public function up() 'ip' => ['type' => 'VARCHAR', 'constraint' => 100], 'ip2' => ['type' => 'VARCHAR', 'constraint' => 100], ])->createTable('ip_table', true); + + // Database session table + if ($this->db->DBDriver === 'MySQLi') { + $this->forge->addField([ + 'id' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => false], + 'ip_address' => ['type' => 'VARCHAR', 'constraint' => 45, 'null' => false], + 'timestamp timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL', + 'data' => ['type' => 'BLOB', 'null' => false], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('timestamp'); + $this->forge->createTable('ci_sessions', true); + } + + if ($this->db->DBDriver === 'Postgre') { + $this->forge->addField([ + 'id' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => false], + 'ip_address inet NOT NULL', + 'timestamp timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL', + "data bytea DEFAULT '' NOT NULL", + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('timestamp'); + $this->forge->createTable('ci_sessions', true); + } } public function down() @@ -140,5 +165,9 @@ public function down() $this->forge->dropTable('stringifypkey', true); $this->forge->dropTable('without_auto_increment', true); $this->forge->dropTable('ip_table', true); + + if (in_array($this->db->DBDriver, ['MySQLi', 'Postgre'], true)) { + $this->forge->dropTable('ci_sessions', true); + } } } diff --git a/tests/_support/Database/Seeds/CITestSeeder.php b/tests/_support/Database/Seeds/CITestSeeder.php index 0319896159ab..ae4b2af3b0b7 100644 --- a/tests/_support/Database/Seeds/CITestSeeder.php +++ b/tests/_support/Database/Seeds/CITestSeeder.php @@ -146,6 +146,24 @@ public function run() ); } + if ($this->db->DBDriver === 'MySQLi') { + $data['ci_sessions'][] = [ + 'id' => '1f5o06b43phsnnf8if6bo33b635e4p2o', + 'ip_address' => '127.0.0.1', + 'timestamp' => '2021-06-25 21:54:14', + 'data' => '__ci_last_regenerate|i:1624650854;_ci_previous_url|s:40:\"http://localhost/index.php/home/index\";', + ]; + } + + if ($this->db->DBDriver === 'Postgre') { + $data['ci_sessions'][] = [ + 'id' => '1f5o06b43phsnnf8if6bo33b635e4p2o', + 'ip_address' => '127.0.0.1', + 'timestamp' => '2021-06-25 21:54:14.991403+02', + 'data' => '\x' . bin2hex('__ci_last_regenerate|i:1624650854;_ci_previous_url|s:40:\"http://localhost/index.php/home/index\";'), + ]; + } + foreach ($data as $table => $dummy_data) { $this->db->table($table)->truncate(); diff --git a/tests/system/Database/Live/MetadataTest.php b/tests/system/Database/Live/MetadataTest.php index e59b28f7b724..8c6806c4d86e 100644 --- a/tests/system/Database/Live/MetadataTest.php +++ b/tests/system/Database/Live/MetadataTest.php @@ -52,6 +52,10 @@ protected function setUp(): void $prefix . 'without_auto_increment', $prefix . 'ip_table', ]; + + if (in_array($this->db->DBDriver, ['MySQLi', 'Postgre'], true)) { + $this->expectedTables[] = $prefix . 'ci_sessions'; + } } public function testListTables() diff --git a/tests/system/Session/Handlers/DatabaseHandlerTest.php b/tests/system/Session/Handlers/DatabaseHandlerTest.php new file mode 100644 index 000000000000..7e69bf8e6c73 --- /dev/null +++ b/tests/system/Session/Handlers/DatabaseHandlerTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Session\Handlers; + +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\DatabaseTestTrait; +use CodeIgniter\Test\ReflectionHelper; +use Config\App as AppConfig; +use Config\Database as DatabaseConfig; + +/** + * @internal + */ +final class DatabaseHandlerTest extends CIUnitTestCase +{ + use DatabaseTestTrait; + use ReflectionHelper; + + protected $refresh = true; + + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + + protected function setUp(): void + { + parent::setUp(); + + if (! in_array(config(DatabaseConfig::class)->tests['DBDriver'], ['MySQLi', 'Postgre'], true)) { + $this->markTestSkipped('Database Session Handler requires database driver to be MySQLi or Postgre'); + } + } + + protected function getInstance($options = []) + { + $defaults = [ + 'sessionDriver' => 'CodeIgniter\Session\Handlers\DatabaseHandler', + 'sessionCookieName' => 'ci_session', + 'sessionExpiration' => 7200, + 'sessionSavePath' => 'ci_sessions', + 'sessionMatchIP' => false, + 'sessionTimeToUpdate' => 300, + 'sessionRegenerateDestroy' => false, + 'cookieDomain' => '', + 'cookiePrefix' => '', + 'cookiePath' => '/', + 'cookieSecure' => false, + 'cookieSameSite' => 'Lax', + ]; + + $config = array_merge($defaults, $options); + $appConfig = new AppConfig(); + + foreach ($config as $key => $c) { + $appConfig->{$key} = $c; + } + + return new DatabaseHandler($appConfig, '127.0.0.1'); + } + + public function testOpen() + { + $handler = $this->getInstance(); + $this->assertTrue($handler->open('ci_sessions', 'ci_session')); + } + + public function testReadSuccess() + { + $handler = $this->getInstance(); + $expected = '__ci_last_regenerate|i:1624650854;_ci_previous_url|s:40:\"http://localhost/index.php/home/index\";'; + $this->assertSame($expected, $handler->read('1f5o06b43phsnnf8if6bo33b635e4p2o')); + + $this->assertTrue($this->getPrivateProperty($handler, 'rowExists')); + $this->assertSame('1483201a66afd2bd671e4a67dc6ecf24', $this->getPrivateProperty($handler, 'fingerprint')); + } + + public function testReadFailure() + { + $handler = $this->getInstance(); + $this->assertSame('', $handler->read('123456b43phsnnf8if6bo33b635e4321')); + + $this->assertFalse($this->getPrivateProperty($handler, 'rowExists')); + $this->assertSame('d41d8cd98f00b204e9800998ecf8427e', $this->getPrivateProperty($handler, 'fingerprint')); + } + + public function testWriteInsert() + { + $handler = $this->getInstance(); + + $this->setPrivateProperty($handler, 'lock', true); + + $data = '__ci_last_regenerate|i:1624650854;_ci_previous_url|s:40:\"http://localhost/index.php/home/index\";'; + $this->assertTrue($handler->write('555556b43phsnnf8if6bo33b635e4444', $data)); + + $this->setPrivateProperty($handler, 'lock', false); + + $row = $this->db->table('ci_sessions') + ->getWhere(['id' => '555556b43phsnnf8if6bo33b635e4444']) + ->getRow(); + + $this->assertGreaterThan(time() - 100, strtotime($row->timestamp)); + $this->assertSame('1483201a66afd2bd671e4a67dc6ecf24', $this->getPrivateProperty($handler, 'fingerprint')); + } + + public function testWriteUpdate() + { + $handler = $this->getInstance(); + + $this->setPrivateProperty($handler, 'sessionID', '1f5o06b43phsnnf8if6bo33b635e4p2o'); + $this->setPrivateProperty($handler, 'rowExists', true); + + $lockSession = $this->getPrivateMethodInvoker($handler, 'lockSession'); + $lockSession('1f5o06b43phsnnf8if6bo33b635e4p2o'); + + $data = '__ci_last_regenerate|i:1624650854;_ci_previous_url|s:40:\"http://localhost/index.php/home/index\";'; + $this->assertTrue($handler->write('1f5o06b43phsnnf8if6bo33b635e4p2o', $data)); + + $releaseLock = $this->getPrivateMethodInvoker($handler, 'releaseLock'); + $releaseLock(); + + $row = $this->db->table('ci_sessions') + ->getWhere(['id' => '1f5o06b43phsnnf8if6bo33b635e4p2o']) + ->getRow(); + + $this->assertGreaterThan(time() - 100, strtotime($row->timestamp)); + $this->assertSame('1483201a66afd2bd671e4a67dc6ecf24', $this->getPrivateProperty($handler, 'fingerprint')); + } + + public function testGC() + { + $handler = $this->getInstance(); + $this->assertTrue($handler->gc(3600)); + } +} From 860003281275d68dd3b849a783036b5293452cf8 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sat, 26 Jun 2021 11:14:18 +0200 Subject: [PATCH 029/490] Fix for generating session table structure via migrations --- .../Generators/MigrationGenerator.php | 9 +++---- .../Generators/Views/migration.tpl.php | 24 ++++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/system/Commands/Generators/MigrationGenerator.php b/system/Commands/Generators/MigrationGenerator.php index 1cd931e9e460..24ab3ea72330 100644 --- a/system/Commands/Generators/MigrationGenerator.php +++ b/system/Commands/Generators/MigrationGenerator.php @@ -101,10 +101,11 @@ protected function prepare(string $class): string $table = $this->getOption('table'); $DBGroup = $this->getOption('dbgroup'); - $data['session'] = true; - $data['table'] = is_string($table) ? $table : 'ci_sessions'; - $data['DBGroup'] = is_string($DBGroup) ? $DBGroup : 'default'; - $data['matchIP'] = config('App')->sessionMatchIP; + $data['session'] = true; + $data['table'] = is_string($table) ? $table : 'ci_sessions'; + $data['DBGroup'] = is_string($DBGroup) ? $DBGroup : 'default'; + $data['DBDriver'] = config('Database')->{$data['DBGroup']}['DBDriver']; + $data['matchIP'] = config('App')->sessionMatchIP; } return $this->parseTemplate($class, [], [], $data); diff --git a/system/Commands/Generators/Views/migration.tpl.php b/system/Commands/Generators/Views/migration.tpl.php index 436ee85e04cc..321895e670c2 100644 --- a/system/Commands/Generators/Views/migration.tpl.php +++ b/system/Commands/Generators/Views/migration.tpl.php @@ -12,17 +12,23 @@ class {class} extends Migration public function up() { $this->forge->addField([ - 'id' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => false], + 'id' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => false], + 'ip_address' => ['type' => 'VARCHAR', 'constraint' => 45, 'null' => false], - 'timestamp' => ['type' => 'INT', 'unsigned' => true, 'null' => false, 'default' => 0], - 'data' => ['type' => 'TEXT', 'null' => false, 'default' => ''], + 'timestamp timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL', + 'data' => ['type' => 'BLOB', 'null' => false], + + 'ip_address inet NOT NULL', + 'timestamp timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL', + "data bytea DEFAULT '' NOT NULL", + ]); - - $this->forge->addKey(['id', 'ip_address'], true); - - $this->forge->addKey('id', true); - - $this->forge->addKey('timestamp'); + + $this->forge->addKey(['id', 'ip_address'], true); + + $this->forge->addKey('id', true); + + $this->forge->addKey('timestamp'); $this->forge->createTable('', true); } From bacda111b9a1cfe019de22a0420493ac39f80b21 Mon Sep 17 00:00:00 2001 From: MGatner Date: Sun, 13 Jun 2021 23:05:43 +0000 Subject: [PATCH 030/490] Implement FileCollection --- system/Files/Exceptions/FileException.php | 20 + system/Files/FileCollection.php | 386 +++++++++++++ system/Language/en/Files.php | 6 +- system/Language/en/Publisher.php | 2 - .../Exceptions/PublisherException.php | 20 - system/Publisher/Publisher.php | 308 +--------- tests/system/Files/FileCollectionTest.php | 536 ++++++++++++++++++ tests/system/Publisher/PublisherInputTest.php | 338 +---------- .../system/Publisher/PublisherSupportTest.php | 68 +-- user_guide_src/source/libraries/files.rst | 96 ++++ user_guide_src/source/libraries/publisher.rst | 56 +- 11 files changed, 1066 insertions(+), 770 deletions(-) create mode 100644 system/Files/FileCollection.php create mode 100644 tests/system/Files/FileCollectionTest.php diff --git a/system/Files/Exceptions/FileException.php b/system/Files/Exceptions/FileException.php index bbbdc1a3cbb1..40e9fdc20f41 100644 --- a/system/Files/Exceptions/FileException.php +++ b/system/Files/Exceptions/FileException.php @@ -23,4 +23,24 @@ public static function forUnableToMove(?string $from = null, ?string $to = null, { return new static(lang('Files.cannotMove', [$from, $to, $error])); } + + /** + * Throws when an item is expected to be a directory but is not or is missing. + * + * @param string $caller The method causing the exception + */ + public static function forExpectedDirectory(string $caller) + { + return new static(lang('Files.expectedDirectory', [$caller])); + } + + /** + * Throws when an item is expected to be a file but is not or is missing. + * + * @param string $caller The method causing the exception + */ + public static function forExpectedFile(string $caller) + { + return new static(lang('Files.expectedFile', [$caller])); + } } diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php new file mode 100644 index 000000000000..bf4eeaf6b836 --- /dev/null +++ b/system/Files/FileCollection.php @@ -0,0 +1,386 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeIgniter\Files; + +use CodeIgniter\Files\Exceptions\FileException; +use CodeIgniter\Files\Exceptions\FileNotFoundException; +use Countable; +use Generator; +use InvalidArgumentException; +use IteratorAggregate; +use Traversable; + +/** + * File Collection Class + * + * Representation for a group of files, with utilities for locating, + * filtering, and ordering them. + */ +class FileCollection implements Countable, IteratorAggregate +{ + /** + * The current list of file paths. + * + * @var string[] + */ + protected $files = []; + + //-------------------------------------------------------------------- + + /** + * Resolves a full path and verifies it is an actual directory. + * + * @param string $directory + * + * @return string + * + * @throws FileException + */ + protected static function resolveDirectory(string $directory): string + { + if (! is_dir($directory = set_realpath($directory))) + { + $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + throw FileException::forExpectedDirectory($caller['function']); + } + + return $directory; + } + + /** + * Resolves a full path and verifies it is an actual file. + * + * @param string $file + * + * @return string + * + * @throws FileException + */ + protected static function resolveFile(string $file): string + { + if (! is_file($file = set_realpath($file))) + { + $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + throw FileException::forExpectedFile($caller['function']); + } + + return $file; + } + + /** + * Removes files that are not part of the given directory (recursive). + * + * @param string[] $files + * @param string $directory + * + * @return string[] + */ + protected static function filterFiles(array $files, string $directory): array + { + $directory = self::resolveDirectory($directory); + + return array_filter($files, function ($value) use ($directory) { + return strpos($value, $directory) === 0; + }); + } + + /** + * Returns any files whose `basename` matches the given pattern. + * + * @param string[] $files + * @param string $pattern Regex or pseudo-regex string + * + * @return string[] + */ + protected static function matchFiles(array $files, string $pattern): array + { + // Convert pseudo-regex into their true form + if (@preg_match($pattern, null) === false) // @phpstan-ignore-line + { + $pattern = str_replace( + ['#', '.', '*', '?'], + ['\#', '\.', '.*', '.'], + $pattern + ); + $pattern = "#{$pattern}#"; + } + + return array_filter($files, function ($value) use ($pattern) { + return (bool) preg_match($pattern, basename($value)); + }); + } + + //-------------------------------------------------------------------- + + /** + * Loads the Filesystem helper and stores initial files. + * + * @param string[] $files + */ + public function __construct(array $files = []) + { + helper(['filesystem']); + + $this->set($files); + } + + /** + * Optimizes and returns the current file list. + * + * @return string[] + */ + public function get(): array + { + $this->files = array_unique($this->files, SORT_STRING); + sort($this->files, SORT_STRING); + + return $this->files; + } + + /** + * Sets the file list directly, files are still subject to verification. + * This works as a "reset" method with []. + * + * @param string[] $files The new file list to use + * + * @return $this + */ + public function set(array $files) + { + $this->files = []; + + return $this->addFiles($files); + } + + /** + * Adds an array/single file or directory to the list. + * + * @param string|string[] $paths + * @param boolean $recursive + * + * @return $this + */ + public function add($paths, bool $recursive = true) + { + if (! is_array($paths)) + { + $paths = [$paths]; + } + + foreach ($paths as $path) + { + if (! is_string($path)) + { + throw new InvalidArgumentException('FileCollection paths must be strings.'); + } + + // Test for a directory + try + { + $directory = self::resolveDirectory($path); + } + catch (FileException $e) + { + return $this->addFile($path); + } + + $this->addDirectory($path, $recursive); + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Verifies and adds files to the list. + * + * @param string[] $files + * + * @return $this + */ + public function addFiles(array $files) + { + foreach ($files as $file) + { + $this->addFile($file); + } + + return $this; + } + + /** + * Verifies and adds a single file to the file list. + * + * @param string $file + * + * @return $this + */ + public function addFile(string $file) + { + $this->files[] = self::resolveFile($file); + + return $this; + } + + /** + * Removes files from the list. + * + * @param string[] $files + * + * @return $this + */ + public function removeFiles(array $files) + { + $this->files = array_diff($this->files, $files); + + return $this; + } + + /** + * Removes a single file from the list. + * + * @param string $file + * + * @return $this + */ + public function removeFile(string $file) + { + return $this->removeFiles([$file]); + } + + //-------------------------------------------------------------------- + + /** + * Verifies and adds files from each + * directory to the list. + * + * @param string[] $directories + * @param bool $recursive + * + * @return $this + */ + public function addDirectories(array $directories, bool $recursive = false) + { + foreach ($directories as $directory) + { + $this->addDirectory($directory, $recursive); + } + + return $this; + } + + /** + * Verifies and adds all files from a directory. + * + * @param string $directory + * @param boolean $recursive + * + * @return $this + */ + public function addDirectory(string $directory, bool $recursive = false) + { + $directory = self::resolveDirectory($directory); + + // Map the directory to depth 2 to so directories become arrays + foreach (directory_map($directory, 2, true) as $key => $path) + { + if (is_string($path)) + { + $this->addFile($directory . $path); + } + elseif ($recursive && is_array($path)) + { + $this->addDirectory($directory . $key, true); + } + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Removes any files from the list that match the supplied pattern + * (within the optional scope). + * + * @param string $pattern Regex or pseudo-regex string + * @param string|null $scope The directory to limit the scope + * + * @return $this + */ + public function removePattern(string $pattern, string $scope = null) + { + if ($pattern === '') + { + return $this; + } + + // Start with all files or those in scope + $files = is_null($scope) ? $this->files : self::filterFiles($this->files, $scope); + + // Remove any files that match the pattern + return $this->removeFiles(self::matchFiles($files, $pattern)); + } + + /** + * Keeps only the files from the list that match + * (within the optional scope). + * + * @param string $pattern Regex or pseudo-regex string + * @param string|null $scope A directory to limit the scope + * + * @return $this + */ + public function retainPattern(string $pattern, string $scope = null) + { + if ($pattern === '') + { + return $this; + } + + // Start with all files or those in scope + $files = is_null($scope) ? $this->files : self::filterFiles($this->files, $scope); + + // Matches the pattern within the scoped files and remove their inverse. + return $this->removeFiles(array_diff($files, self::matchFiles($files, $pattern))); + } + + //-------------------------------------------------------------------- + + /** + * Returns the current number of files in the collection. + * Fulfills Countable. + * + * @return int + */ + public function count(): int + { + return count($this->files); + } + + /** + * Yields as an Iterator for the current files. + * Fulfills IteratorAggregate. + * + * @throws FileNotFoundException + * + * @return Generator + */ + public function getIterator(): Generator + { + foreach ($this->get() as $file) + { + yield new File($file, true); + } + } +} diff --git a/system/Language/en/Files.php b/system/Language/en/Files.php index 924e98ea354a..bcbc39d7e349 100644 --- a/system/Language/en/Files.php +++ b/system/Language/en/Files.php @@ -11,6 +11,8 @@ // Files language settings return [ - 'fileNotFound' => 'File not found: {0}', - 'cannotMove' => 'Could not move file {0} to {1} ({2}).', + 'fileNotFound' => 'File not found: {0}', + 'cannotMove' => 'Could not move file {0} to {1} ({2}).', + 'expectedDirectory' => '{0} expects a valid directory.', + 'expectedFile' => '{0} expects a valid file.', ]; diff --git a/system/Language/en/Publisher.php b/system/Language/en/Publisher.php index 2d7ae8418b25..77d805a78ff2 100644 --- a/system/Language/en/Publisher.php +++ b/system/Language/en/Publisher.php @@ -12,8 +12,6 @@ // Publisher language settings return [ 'collision' => 'Publisher encountered an unexpected {0} while copying {1} to {2}.', - 'expectedDirectory' => 'Publisher::{0} expects a valid directory.', - 'expectedFile' => 'Publisher::{0} expects a valid file.', 'destinationNotAllowed' => 'Destination is not on the allowed list of Publisher directories: {0}', 'fileNotAllowed' => '{0} fails the following restriction for {1}: {2}', diff --git a/system/Publisher/Exceptions/PublisherException.php b/system/Publisher/Exceptions/PublisherException.php index d54420881ec7..c3e85a994ebe 100644 --- a/system/Publisher/Exceptions/PublisherException.php +++ b/system/Publisher/Exceptions/PublisherException.php @@ -31,26 +31,6 @@ public static function forCollision(string $from, string $to) return new static(lang('Publisher.collision', [filetype($to), $from, $to])); } - /** - * Throws when an object is expected to be a directory but is not or is missing. - * - * @param string $caller The method causing the exception - */ - public static function forExpectedDirectory(string $caller) - { - return new static(lang('Publisher.expectedDirectory', [$caller])); - } - - /** - * Throws when an object is expected to be a file but is not or is missing. - * - * @param string $caller The method causing the exception - */ - public static function forExpectedFile(string $caller) - { - return new static(lang('Publisher.expectedFile', [$caller])); - } - /** * Throws when given a destination that is not in the list of allowed directories. * diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index 1bfd4b91768d..1f28bc331baf 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -12,7 +12,7 @@ namespace CodeIgniter\Publisher; use CodeIgniter\Autoloader\FileLocator; -use CodeIgniter\Files\File; +use CodeIgniter\Files\FileCollection; use CodeIgniter\HTTP\URI; use CodeIgniter\Publisher\Exceptions\PublisherException; use RuntimeException; @@ -28,13 +28,13 @@ * path to a verified file while a "path" is relative to its source * or destination and may indicate either a file or directory of * unconfirmed existence. - * class failures throw the PublisherException, but some underlying + * Class failures throw the PublisherException, but some underlying * methods may percolate different exceptions, like FileException, * FileNotFoundException or InvalidArgumentException. * Write operations will catch all errors in the file-specific * $errors property to minimize impact of partial batch operations. */ -class Publisher +class Publisher extends FileCollection { /** * Array of discovered Publishers. @@ -51,13 +51,6 @@ class Publisher */ private $scratch; - /** - * The current list of files. - * - * @var string[] - */ - private $files = []; - /** * Exceptions for specific files from the last write operation. * @@ -94,6 +87,8 @@ class Publisher */ protected $destination = FCPATH; + //-------------------------------------------------------------------- + /** * Discovers and returns all Publishers in the specified namespace directory. * @@ -134,95 +129,6 @@ final public static function discover(string $directory = 'Publishers'): array return self::$discovered[$directory]; } - //-------------------------------------------------------------------- - - /** - * Resolves a full path and verifies it is an actual directory. - * - * @param string $directory - * - * @return string - * - * @throws PublisherException - */ - private static function resolveDirectory(string $directory): string - { - if (! is_dir($directory = set_realpath($directory))) - { - $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; - throw PublisherException::forExpectedDirectory($caller['function']); - } - - return $directory; - } - - /** - * Resolves a full path and verifies it is an actual file. - * - * @param string $file - * - * @return string - * - * @throws PublisherException - */ - private static function resolveFile(string $file): string - { - if (! is_file($file = set_realpath($file))) - { - $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; - throw PublisherException::forExpectedFile($caller['function']); - } - - return $file; - } - - //-------------------------------------------------------------------- - - /** - * Removes files that are not part of the given directory (recursive). - * - * @param string[] $files - * @param string $directory - * - * @return string[] - */ - private static function filterFiles(array $files, string $directory): array - { - $directory = self::resolveDirectory($directory); - - return array_filter($files, function ($value) use ($directory) { - return strpos($value, $directory) === 0; - }); - } - - /** - * Returns any files whose `basename` matches the given pattern. - * - * @param array $files - * @param string $pattern Regex or pseudo-regex string - * - * @return string[] - */ - private static function matchFiles(array $files, string $pattern): array - { - // Convert pseudo-regex into their true form - if (@preg_match($pattern, null) === false) // @phpstan-ignore-line - { - $pattern = str_replace( - ['#', '.', '*', '?'], - ['\#', '\.', '.*', '.'], - $pattern - ); - $pattern = "#{$pattern}#"; - } - - return array_filter($files, function ($value) use ($pattern) { - return (bool) preg_match($pattern, basename($value)); - }); - } - - //-------------------------------------------------------------------- - /* * Removes a directory and all its files and subdirectories. * @@ -305,7 +211,7 @@ public function publish(): bool // Safeguard against accidental misuse if ($this->source === ROOTPATH && $this->destination === FCPATH) { - throw new RuntimeException('Child classes of Publisher should provide their own source and destination or publish method.'); + throw new RuntimeException('Child classes of Publisher should provide their own publish method or a source and destination.'); } return $this->addPath('/')->merge(true); @@ -369,142 +275,6 @@ final public function getPublished(): array return $this->published; } - /** - * Optimizes and returns the current file list. - * - * @return string[] - */ - final public function getFiles(): array - { - $this->files = array_unique($this->files, SORT_STRING); - sort($this->files, SORT_STRING); - - return $this->files; - } - - //-------------------------------------------------------------------- - - /** - * Sets the file list directly, files are still subject to verification. - * This works as a "reset" method with []. - * - * @param string[] $files The new file list to use - * - * @return $this - */ - final public function setFiles(array $files) - { - $this->files = []; - - return $this->addFiles($files); - } - - /** - * Verifies and adds files to the list. - * - * @param string[] $files - * - * @return $this - */ - final public function addFiles(array $files) - { - foreach ($files as $file) - { - $this->addFile($file); - } - - return $this; - } - - /** - * Verifies and adds a single file to the file list. - * - * @param string $file - * - * @return $this - */ - final public function addFile(string $file) - { - $this->files[] = self::resolveFile($file); - - return $this; - } - - /** - * Removes files from the list. - * - * @param string[] $files - * - * @return $this - */ - final public function removeFiles(array $files) - { - $this->files = array_diff($this->files, $files); - - return $this; - } - - /** - * Removes a single file from the list. - * - * @param string $file - * - * @return $this - */ - final public function removeFile(string $file) - { - return $this->removeFiles([$file]); - } - - //-------------------------------------------------------------------- - - /** - * Verifies and adds files from each - * directory to the list. - * - * @param string[] $directories - * @param bool $recursive - * - * @return $this - */ - final public function addDirectories(array $directories, bool $recursive = false) - { - foreach ($directories as $directory) - { - $this->addDirectory($directory, $recursive); - } - - return $this; - } - - /** - * Verifies and adds all files from a directory. - * - * @param string $directory - * @param boolean $recursive - * - * @return $this - */ - final public function addDirectory(string $directory, bool $recursive = false) - { - $directory = self::resolveDirectory($directory); - - // Map the directory to depth 2 to so directories become arrays - foreach (directory_map($directory, 2, true) as $key => $path) - { - if (is_string($path)) - { - $this->addFile($directory . $path); - } - elseif ($recursive && is_array($path)) - { - $this->addDirectory($directory . $key, true); - } - } - - return $this; - } - //-------------------------------------------------------------------- /** @@ -535,19 +305,9 @@ final public function addPaths(array $paths, bool $recursive = true) */ final public function addPath(string $path, bool $recursive = true) { - $full = $this->source . $path; - - // Test for a directory - try - { - $directory = self::resolveDirectory($full); - } - catch (PublisherException $e) - { - return $this->addFile($full); - } + $this->add($this->source . $path, $recursive); - return $this->addDirectory($full, $recursive); + return $this; } //-------------------------------------------------------------------- @@ -589,54 +349,6 @@ final public function addUri(string $uri) //-------------------------------------------------------------------- - /** - * Removes any files from the list that match the supplied pattern - * (within the optional scope). - * - * @param string $pattern Regex or pseudo-regex string - * @param string|null $scope The directory to limit the scope - * - * @return $this - */ - final public function removePattern(string $pattern, string $scope = null) - { - if ($pattern === '') - { - return $this; - } - - // Start with all files or those in scope - $files = is_null($scope) ? $this->files : self::filterFiles($this->files, $scope); - - // Remove any files that match the pattern - return $this->removeFiles(self::matchFiles($files, $pattern)); - } - - /** - * Keeps only the files from the list that match - * (within the optional scope). - * - * @param string $pattern Regex or pseudo-regex string - * @param string|null $scope A directory to limit the scope - * - * @return $this - */ - final public function retainPattern(string $pattern, string $scope = null) - { - if ($pattern === '') - { - return $this; - } - - // Start with all files or those in scope - $files = is_null($scope) ? $this->files : self::filterFiles($this->files, $scope); - - // Matches the pattern within the scoped files and remove their inverse. - return $this->removeFiles(array_diff($files, self::matchFiles($files, $pattern))); - } - - //-------------------------------------------------------------------- - /** * Removes the destination and all its files and folders. * @@ -662,7 +374,7 @@ final public function copy(bool $replace = true): bool { $this->errors = $this->published = []; - foreach ($this->getFiles() as $file) + foreach ($this->get() as $file) { $to = $this->destination . basename($file); @@ -693,7 +405,7 @@ final public function merge(bool $replace = true): bool $this->errors = $this->published = []; // Get the files from source for special handling - $sourced = self::filterFiles($this->getFiles(), $this->source); + $sourced = self::filterFiles($this->get(), $this->source); // Handle everything else with a flat copy $this->files = array_diff($this->files, $sourced); diff --git a/tests/system/Files/FileCollectionTest.php b/tests/system/Files/FileCollectionTest.php new file mode 100644 index 000000000000..3e77941d5d2f --- /dev/null +++ b/tests/system/Files/FileCollectionTest.php @@ -0,0 +1,536 @@ +getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory'); + + $this->assertSame($this->directory, $method($this->directory)); + } + + public function testResolveDirectoryFile() + { + $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory'); + + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedDirectory', ['invokeArgs'])); + + $method($this->file); + } + + public function testResolveDirectorySymlink() + { + // Create a symlink to test + $link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4)); + symlink($this->directory, $link); + + $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory'); + + $this->assertSame($this->directory, $method($link)); + + unlink($link); + } + + //-------------------------------------------------------------------- + + public function testResolveFileFile() + { + $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile'); + + $this->assertSame($this->file, $method($this->file)); + } + + public function testResolveFileSymlink() + { + // Create a symlink to test + $link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4)); + symlink($this->file, $link); + + $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile'); + + $this->assertSame($this->file, $method($link)); + + unlink($link); + } + + public function testResolveFileDirectory() + { + $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile'); + + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedFile', ['invokeArgs'])); + + $method($this->directory); + } + + //-------------------------------------------------------------------- + + public function testAddStringFile() + { + $files = new FileCollection(); + + $files->add(SUPPORTPATH . 'Files/baker/banana.php'); + + $this->assertSame([$this->file], $files->get()); + } + + public function testAddStringFileRecursiveDoesNothing() + { + $files = new FileCollection(); + + $files->add(SUPPORTPATH . 'Files/baker/banana.php', true); + + $this->assertSame([$this->file], $files->get()); + } + + public function testAddStringDirectory() + { + $files = new FileCollection(); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + ]; + + $files->add(SUPPORTPATH . 'Files/able'); + + $this->assertSame($expected, $files->get()); + } + + public function testAddStringDirectoryRecursive() + { + $files = new FileCollection(); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $files->add(SUPPORTPATH . 'Files'); + + $this->assertSame($expected, $files->get()); + } + + public function testAddArray() + { + $files = new FileCollection(); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $files->add([ + SUPPORTPATH . 'Files/able', + SUPPORTPATH . 'Files/baker/banana.php', + ]); + + $this->assertSame($expected, $files->get()); + } + + public function testAddArrayRecursive() + { + $files = new FileCollection(); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + SUPPORTPATH . 'Log/Handlers/TestHandler.php', + ]; + + $files->add([ + SUPPORTPATH . 'Files', + SUPPORTPATH . 'Log', + ], true); + + $this->assertSame($expected, $files->get()); + } + + //-------------------------------------------------------------------- + + public function testAddFile() + { + $collection = new FileCollection(); + $this->assertSame([], $this->getPrivateProperty($collection, 'files')); + + $collection->addFile($this->file); + $this->assertSame([$this->file], $this->getPrivateProperty($collection, 'files')); + } + + public function testAddFileMissing() + { + $collection = new FileCollection(); + + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedFile', ['addFile'])); + + $collection->addFile('TheHillsAreAlive.bmp'); + } + + public function testAddFileDirectory() + { + $collection = new FileCollection(); + + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedFile', ['addFile'])); + + $collection->addFile($this->directory); + } + + public function testAddFiles() + { + $collection = new FileCollection(); + $files = [ + $this->file, + $this->file, + ]; + + $collection->addFiles($files); + $this->assertSame($files, $this->getPrivateProperty($collection, 'files')); + } + + //-------------------------------------------------------------------- + + public function testGet() + { + $collection = new FileCollection(); + $collection->addFile($this->file); + + $this->assertSame([$this->file], $collection->get()); + } + + public function testGetSorts() + { + $collection = new FileCollection(); + $files = [ + $this->file, + $this->directory . 'apple.php', + ]; + + $collection->addFiles($files); + + $this->assertSame(array_reverse($files), $collection->get()); + } + + public function testGetUniques() + { + $collection = new FileCollection(); + $files = [ + $this->file, + $this->file, + ]; + + $collection->addFiles($files); + $this->assertSame([$this->file], $collection->get()); + } + + public function testSet() + { + $collection = new FileCollection(); + + $collection->set([$this->file]); + $this->assertSame([$this->file], $collection->get()); + } + + public function testSetInvalid() + { + $collection = new FileCollection(); + + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedFile', ['addFile'])); + + $collection->set(['flerb']); + } + + //-------------------------------------------------------------------- + + public function testRemoveFile() + { + $collection = new FileCollection(); + $files = [ + $this->file, + $this->directory . 'apple.php', + ]; + + $collection->addFiles($files); + + $collection->removeFile($this->file); + + $this->assertSame([$this->directory . 'apple.php'], $collection->get()); + } + + public function testRemoveFiles() + { + $collection = new FileCollection(); + $files = [ + $this->file, + $this->directory . 'apple.php', + ]; + + $collection->addFiles($files); + + $collection->removeFiles($files); + + $this->assertSame([], $collection->get()); + } + + //-------------------------------------------------------------------- + + public function testAddDirectoryInvalid() + { + $collection = new FileCollection(); + + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedDirectory', ['addDirectory'])); + + $collection->addDirectory($this->file); + } + + public function testAddDirectory() + { + $collection = new FileCollection(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + ]; + + $collection->addDirectory($this->directory); + + $this->assertSame($expected, $collection->get()); + } + + public function testAddDirectoryRecursive() + { + $collection = new FileCollection(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $this->assertSame($expected, $collection->get()); + } + + public function testAddDirectories() + { + $collection = new FileCollection(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $collection->addDirectories([ + $this->directory, + SUPPORTPATH . 'Files/baker', + ]); + + $this->assertSame($expected, $collection->get()); + } + + public function testAddDirectoriesRecursive() + { + $collection = new FileCollection(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + SUPPORTPATH . 'Log/Handlers/TestHandler.php', + ]; + + $collection->addDirectories([ + SUPPORTPATH . 'Files', + SUPPORTPATH . 'Log', + ], true); + + $this->assertSame($expected, $collection->get()); + } + + //-------------------------------------------------------------------- + + public function testRemovePatternEmpty() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $files = $collection->get(); + + $collection->removePattern(''); + + $this->assertSame($files, $collection->get()); + } + + public function testRemovePatternRegex() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + $this->directory . 'apple.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $collection->removePattern('#[a-z]+_.*#'); + + $this->assertSame($expected, $collection->get()); + } + + public function testRemovePatternPseudo() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + $this->directory . 'apple.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $collection->removePattern('*_*.php'); + + $this->assertSame($expected, $collection->get()); + } + + public function testRemovePatternScope() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $collection->removePattern('*.php', $this->directory); + + $this->assertSame($expected, $collection->get()); + } + + //-------------------------------------------------------------------- + + public function testRetainPatternEmpty() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $files = $collection->get(); + + $collection->retainPattern(''); + + $this->assertSame($files, $collection->get()); + } + + public function testRetainPatternRegex() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + ]; + + $collection->retainPattern('#[a-z]+_.*#'); + + $this->assertSame($expected, $collection->get()); + } + + public function testRetainPatternPseudo() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + $this->directory . 'fig_3.php', + ]; + + $collection->retainPattern('*_?.php'); + + $this->assertSame($expected, $collection->get()); + } + + public function testRetainPatternScope() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + $this->directory . 'fig_3.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $collection->retainPattern('*_?.php', $this->directory); + + $this->assertSame($expected, $collection->get()); + } + + //-------------------------------------------------------------------- + + public function testCount() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $this->assertCount(4, $collection); + } + + public function testIterable() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $count = 0; + foreach ($collection as $file) + { + $this->assertInstanceOf(File::class, $file); + $count++; + } + + $this->assertSame($count, 4); + } +} diff --git a/tests/system/Publisher/PublisherInputTest.php b/tests/system/Publisher/PublisherInputTest.php index e7450f5c02a9..29d48ef78816 100644 --- a/tests/system/Publisher/PublisherInputTest.php +++ b/tests/system/Publisher/PublisherInputTest.php @@ -35,219 +35,13 @@ public static function setUpBeforeClass(): void //-------------------------------------------------------------------- - public function testAddFile() - { - $publisher = new Publisher(); - $this->assertSame([], $this->getPrivateProperty($publisher, 'files')); - - $publisher->addFile($this->file); - $this->assertSame([$this->file], $this->getPrivateProperty($publisher, 'files')); - } - - public function testAddFileMissing() - { - $publisher = new Publisher(); - - $this->expectException(PublisherException::class); - $this->expectExceptionMessage(lang('Publisher.expectedFile', ['addFile'])); - - $publisher->addFile('TheHillsAreAlive.bmp'); - } - - public function testAddFileDirectory() - { - $publisher = new Publisher(); - - $this->expectException(PublisherException::class); - $this->expectExceptionMessage(lang('Publisher.expectedFile', ['addFile'])); - - $publisher->addFile($this->directory); - } - - public function testAddFiles() - { - $publisher = new Publisher(); - $files = [ - $this->file, - $this->file, - ]; - - $publisher->addFiles($files); - $this->assertSame($files, $this->getPrivateProperty($publisher, 'files')); - } - - //-------------------------------------------------------------------- - - public function testGetFiles() - { - $publisher = new Publisher(); - $publisher->addFile($this->file); - - $this->assertSame([$this->file], $publisher->getFiles()); - } - - public function testGetFilesSorts() - { - $publisher = new Publisher(); - $files = [ - $this->file, - $this->directory . 'apple.php', - ]; - - $publisher->addFiles($files); - - $this->assertSame(array_reverse($files), $publisher->getFiles()); - } - - public function testGetFilesUniques() - { - $publisher = new Publisher(); - $files = [ - $this->file, - $this->file, - ]; - - $publisher->addFiles($files); - $this->assertSame([$this->file], $publisher->getFiles()); - } - - public function testSetFiles() - { - $publisher = new Publisher(); - - $publisher->setFiles([$this->file]); - $this->assertSame([$this->file], $publisher->getFiles()); - } - - public function testSetFilesInvalid() - { - $publisher = new Publisher(); - - $this->expectException(PublisherException::class); - $this->expectExceptionMessage(lang('Publisher.expectedFile', ['addFile'])); - - $publisher->setFiles(['flerb']); - } - - //-------------------------------------------------------------------- - - public function testRemoveFile() - { - $publisher = new Publisher(); - $files = [ - $this->file, - $this->directory . 'apple.php', - ]; - - $publisher->addFiles($files); - - $publisher->removeFile($this->file); - - $this->assertSame([$this->directory . 'apple.php'], $publisher->getFiles()); - } - - public function testRemoveFiles() - { - $publisher = new Publisher(); - $files = [ - $this->file, - $this->directory . 'apple.php', - ]; - - $publisher->addFiles($files); - - $publisher->removeFiles($files); - - $this->assertSame([], $publisher->getFiles()); - } - - //-------------------------------------------------------------------- - - public function testAddDirectoryInvalid() - { - $publisher = new Publisher(); - - $this->expectException(PublisherException::class); - $this->expectExceptionMessage(lang('Publisher.expectedDirectory', ['addDirectory'])); - - $publisher->addDirectory($this->file); - } - - public function testAddDirectory() - { - $publisher = new Publisher(); - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - ]; - - $publisher->addDirectory($this->directory); - - $this->assertSame($expected, $publisher->getFiles()); - } - - public function testAddDirectoryRecursive() - { - $publisher = new Publisher(); - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; - - $publisher->addDirectory(SUPPORTPATH . 'Files', true); - - $this->assertSame($expected, $publisher->getFiles()); - } - - public function testAddDirectories() - { - $publisher = new Publisher(); - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; - - $publisher->addDirectories([ - $this->directory, - SUPPORTPATH . 'Files/baker', - ]); - - $this->assertSame($expected, $publisher->getFiles()); - } - - public function testAddDirectoriesRecursive() - { - $publisher = new Publisher(); - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - SUPPORTPATH . 'Log/Handlers/TestHandler.php', - ]; - - $publisher->addDirectories([ - SUPPORTPATH . 'Files', - SUPPORTPATH . 'Log', - ], true); - - $this->assertSame($expected, $publisher->getFiles()); - } - - //-------------------------------------------------------------------- - public function testAddPathFile() { $publisher = new Publisher(SUPPORTPATH . 'Files'); $publisher->addPath('baker/banana.php'); - $this->assertSame([$this->file], $publisher->getFiles()); + $this->assertSame([$this->file], $publisher->get()); } public function testAddPathFileRecursiveDoesNothing() @@ -256,7 +50,7 @@ public function testAddPathFileRecursiveDoesNothing() $publisher->addPath('baker/banana.php', true); - $this->assertSame([$this->file], $publisher->getFiles()); + $this->assertSame([$this->file], $publisher->get()); } public function testAddPathDirectory() @@ -271,7 +65,7 @@ public function testAddPathDirectory() $publisher->addPath('able'); - $this->assertSame($expected, $publisher->getFiles()); + $this->assertSame($expected, $publisher->get()); } public function testAddPathDirectoryRecursive() @@ -287,7 +81,7 @@ public function testAddPathDirectoryRecursive() $publisher->addPath('Files'); - $this->assertSame($expected, $publisher->getFiles()); + $this->assertSame($expected, $publisher->get()); } public function testAddPaths() @@ -306,7 +100,7 @@ public function testAddPaths() 'baker/banana.php', ]); - $this->assertSame($expected, $publisher->getFiles()); + $this->assertSame($expected, $publisher->get()); } public function testAddPathsRecursive() @@ -326,7 +120,7 @@ public function testAddPathsRecursive() 'Log', ], true); - $this->assertSame($expected, $publisher->getFiles()); + $this->assertSame($expected, $publisher->get()); } //-------------------------------------------------------------------- @@ -338,7 +132,7 @@ public function testAddUri() $scratch = $this->getPrivateProperty($publisher, 'scratch'); - $this->assertSame([$scratch . 'composer.json'], $publisher->getFiles()); + $this->assertSame([$scratch . 'composer.json'], $publisher->get()); } public function testAddUris() @@ -351,122 +145,6 @@ public function testAddUris() $scratch = $this->getPrivateProperty($publisher, 'scratch'); - $this->assertSame([$scratch . 'LICENSE', $scratch . 'composer.json'], $publisher->getFiles()); - } - - //-------------------------------------------------------------------- - - public function testRemovePatternEmpty() - { - $publisher = new Publisher(); - $publisher->addDirectory(SUPPORTPATH . 'Files', true); - - $files = $publisher->getFiles(); - - $publisher->removePattern(''); - - $this->assertSame($files, $publisher->getFiles()); - } - - public function testRemovePatternRegex() - { - $publisher = new Publisher(); - $publisher->addDirectory(SUPPORTPATH . 'Files', true); - - $expected = [ - $this->directory . 'apple.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; - - $publisher->removePattern('#[a-z]+_.*#'); - - $this->assertSame($expected, $publisher->getFiles()); - } - - public function testRemovePatternPseudo() - { - $publisher = new Publisher(); - $publisher->addDirectory(SUPPORTPATH . 'Files', true); - - $expected = [ - $this->directory . 'apple.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; - - $publisher->removePattern('*_*.php'); - - $this->assertSame($expected, $publisher->getFiles()); - } - - public function testRemovePatternScope() - { - $publisher = new Publisher(); - $publisher->addDirectory(SUPPORTPATH . 'Files', true); - - $expected = [ - SUPPORTPATH . 'Files/baker/banana.php', - ]; - - $publisher->removePattern('*.php', $this->directory); - - $this->assertSame($expected, $publisher->getFiles()); - } - - //-------------------------------------------------------------------- - - public function testRetainPatternEmpty() - { - $publisher = new Publisher(); - $publisher->addDirectory(SUPPORTPATH . 'Files', true); - - $files = $publisher->getFiles(); - - $publisher->retainPattern(''); - - $this->assertSame($files, $publisher->getFiles()); - } - - public function testRetainPatternRegex() - { - $publisher = new Publisher(); - $publisher->addDirectory(SUPPORTPATH . 'Files', true); - - $expected = [ - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - ]; - - $publisher->retainPattern('#[a-z]+_.*#'); - - $this->assertSame($expected, $publisher->getFiles()); - } - - public function testRetainPatternPseudo() - { - $publisher = new Publisher(); - $publisher->addDirectory(SUPPORTPATH . 'Files', true); - - $expected = [ - $this->directory . 'fig_3.php', - ]; - - $publisher->retainPattern('*_?.php'); - - $this->assertSame($expected, $publisher->getFiles()); - } - - public function testRetainPatternScope() - { - $publisher = new Publisher(); - $publisher->addDirectory(SUPPORTPATH . 'Files', true); - - $expected = [ - $this->directory . 'fig_3.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; - - $publisher->retainPattern('*_?.php', $this->directory); - - $this->assertSame($expected, $publisher->getFiles()); + $this->assertSame([$scratch . 'LICENSE', $scratch . 'composer.json'], $publisher->get()); } } diff --git a/tests/system/Publisher/PublisherSupportTest.php b/tests/system/Publisher/PublisherSupportTest.php index 42f22d721525..081b831f1241 100644 --- a/tests/system/Publisher/PublisherSupportTest.php +++ b/tests/system/Publisher/PublisherSupportTest.php @@ -53,75 +53,11 @@ public function testDiscoverNothing() public function testDiscoverStores() { $publisher = Publisher::discover()[0]; - $publisher->setFiles([])->addFile($this->file); + $publisher->set([])->addFile($this->file); $result = Publisher::discover(); $this->assertSame($publisher, $result[0]); - $this->assertSame([$this->file], $result[0]->getFiles()); - } - - //-------------------------------------------------------------------- - - public function testResolveDirectoryDirectory() - { - $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveDirectory'); - - $this->assertSame($this->directory, $method($this->directory)); - } - - public function testResolveDirectoryFile() - { - $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveDirectory'); - - $this->expectException(PublisherException::class); - $this->expectExceptionMessage(lang('Publisher.expectedDirectory', ['invokeArgs'])); - - $method($this->file); - } - - public function testResolveDirectorySymlink() - { - // Create a symlink to test - $link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4)); - symlink($this->directory, $link); - - $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveDirectory'); - - $this->assertSame($this->directory, $method($link)); - - unlink($link); - } - - //-------------------------------------------------------------------- - - public function testResolveFileFile() - { - $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveFile'); - - $this->assertSame($this->file, $method($this->file)); - } - - public function testResolveFileSymlink() - { - // Create a symlink to test - $link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4)); - symlink($this->file, $link); - - $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveFile'); - - $this->assertSame($this->file, $method($link)); - - unlink($link); - } - - public function testResolveFileDirectory() - { - $method = $this->getPrivateMethodInvoker(Publisher::class, 'resolveFile'); - - $this->expectException(PublisherException::class); - $this->expectExceptionMessage(lang('Publisher.expectedFile', ['invokeArgs'])); - - $method($this->directory); + $this->assertSame([$this->file], $result[0]->get()); } //-------------------------------------------------------------------- diff --git a/user_guide_src/source/libraries/files.rst b/user_guide_src/source/libraries/files.rst index 9b29767d4d19..ad2c8671a4d5 100644 --- a/user_guide_src/source/libraries/files.rst +++ b/user_guide_src/source/libraries/files.rst @@ -107,3 +107,99 @@ The move() method returns a new File instance that for the relocated file, so yo resulting location is needed:: $file = $file->move(WRITEPATH.'uploads'); + +**************** +File Collections +**************** + +Working with groups of files can be cumbersome, so the framework supplies the ``FileCollection`` class to facilitate +locating and working with groups of files across the filesystem. At its most basic, ``FileCollection`` is an index +of files you set or build:: + + $files = new FileCollection([ + FCPATH . 'index.php', + ROOTPATH . 'spark', + ]); + $files->addDirectory(APPPATH . 'Filters'); + +After you have input the files you would like to work with you may remove files or use the filtering commands to remove +or retain files matching a certain regex or glob-style pattern:: + + $files->removeFile(APPPATH . 'Filters/DevelopToolbar'); + + $files->removePattern('#\.gitkeep#'); + $files->retainPattern('*.php'); + +When your collection is complete, you can use ``get()`` to retrieve the final list of file paths, or take advantage of +``FileCollection`` being countable and iterable to work directly with each ``File``:: + + echo 'My files: ' . implode(PHP_EOL, $files->get()); + echo 'I have ' . count($files) . ' files!'; + + foreach ($files as $file) + { + echo 'Moving ' . $file->getBasename() . ', ' . $file->getSizeByUnit('mb'); + $file->move(WRITABLE . $file->getRandomName()); + } + +Below are the specific methods for working with a ``FileCollection``. + +Inputting Files +=============== + +**set(array $files)** + +Sets the list of input files to the provided string array of file paths. + +**add(string[]|string $paths, bool $recursive = true)** + +Adds all files indicated by the path or array of paths. If the path resolves to a directory then ``$recursive`` +will include sub-directories. + +**addFile(string $file)** +**addFiles(array $files)** + +Adds the file or files to the current list of input files. Files are absolute paths to actual files. + +**removeFile(string $file)** +**removeFiles(array $files)** + +Removes the file or files from the current list of input files. + +**addDirectory(string $directory, bool $recursive = false)** +**addDirectories(array $directories, bool $recursive = false)** + +Adds all files from the directory or directories, optionally recursing into sub-directories. Directories are +absolute paths to actual directories. + +Filtering Files +=============== + +**removePattern(string $pattern, string $scope = null)** +**retainPattern(string $pattern, string $scope = null)** + +Filters the current file list through the pattern (and optional scope), removing or retaining matched +files. ``$pattern`` may be a complete regex (like ``'#[A-Za-z]+\.php#'``) or a pseudo-regex similar +to ``glob()`` (like ``*.css``). +If a ``$scope`` is provided then only files in or under that directory will be considered (i.e. files +outside of ``$scope`` are always retained). When no scope is provided then all files are subject. + +Examples:: + + $files = new FileCollection(); + $files->add(APPPATH . 'Config', true); // Adds all Config files and directories + + $files->removePattern('*tion.php'); // Would remove Encryption.php, Validation.php, and boot/production.php + $files->removePattern('*tion.php', APPPATH . 'Config/boot'); // Would only remove boot/production.php + + $files->retainPattern('#A.+php$#'); // Would keep only Autoload.php + $files->retainPattern('#d.+php$#', APPPATH . 'Config/boot'); // Would keep everything but boot/production.php and boot/testing.php + +Retrieving Files +================ + +**get(): string[]** + +Returns an array of all the loaded input files. + +.. note:: ``FileCollection`` is an ``IteratorAggregate`` so you can work with it directly (e.g. ``foreach ($collection as $file)``). diff --git a/user_guide_src/source/libraries/publisher.rst b/user_guide_src/source/libraries/publisher.rst index 84b430ba144b..dc2ec08347a8 100644 --- a/user_guide_src/source/libraries/publisher.rst +++ b/user_guide_src/source/libraries/publisher.rst @@ -28,8 +28,8 @@ Concept and Usage * How can I update my project when the framework or modules change? * How can components inject new content into existing projects? -At its most basic, publishing amounts to copying a file or files into a project. ``Publisher`` uses fluent-style -command chaining to read, filter, and process input files, then copies or merges them into the target destination. +At its most basic, publishing amounts to copying a file or files into a project. ``Publisher`` extends ``FileCollection`` +to enact fluent-style command chaining to read, filter, and process input files, then copies or merges them into the target destination. You may use ``Publisher`` on demand in your Controllers or other components, or you may stage publications by extending the class and leveraging its discovery with ``spark publish``. @@ -320,6 +320,8 @@ Now when your module users run ``php spark auth:publish`` they will have the fol Library Reference ***************** +.. note:: ``Publisher`` is an extension of :doc:`FileCollection ` so has access to all those methods for reading and filtering files. + Support Methods =============== @@ -346,33 +348,6 @@ files and changes, and this provides the path to a transient, writable directory Returns any errors from the last write operation. The array keys are the files that caused the error, and the values are the Throwable that was caught. Use ``getMessage()`` on the Throwable to get the error message. -**getFiles(): string[]** - -Returns an array of all the loaded input files. - -Inputting Files -=============== - -**setFiles(array $files)** - -Sets the list of input files to the provided string array of file paths. - -**addFile(string $file)** -**addFiles(array $files)** - -Adds the file or files to the current list of input files. Files are absolute paths to actual files. - -**removeFile(string $file)** -**removeFiles(array $files)** - -Removes the file or files from the current list of input files. - -**addDirectory(string $directory, bool $recursive = false)** -**addDirectories(array $directories, bool $recursive = false)** - -Adds all files from the directory or directories, optionally recursing into sub-directories. Directories are -absolute paths to actual directories. - **addPath(string $path, bool $recursive = true)** **addPaths(array $path, bool $recursive = true)** @@ -388,29 +363,6 @@ file to the list. .. note:: The CURL request made is a simple ``GET`` and uses the response body for the file contents. Some remote files may need a custom request to be handled properly. -Filtering Files -=============== - -**removePattern(string $pattern, string $scope = null)** -**retainPattern(string $pattern, string $scope = null)** - -Filters the current file list through the pattern (and optional scope), removing or retaining matched -files. ``$pattern`` may be a complete regex (like ``'#[A-Za-z]+\.php#'``) or a pseudo-regex similar -to ``glob()`` (like ``*.css``). -If a ``$scope`` is provided then only files in or under that directory will be considered (i.e. files -outside of ``$scope`` are always retained). When no scope is provided then all files are subject. - -Examples:: - - $publisher = new Publisher(APPPATH . 'Config'); - $publisher->addPath('/', true); // Adds all Config files and directories - - $publisher->removePattern('*tion.php'); // Would remove Encryption.php, Validation.php, and boot/production.php - $publisher->removePattern('*tion.php', APPPATH . 'Config/boot'); // Would only remove boot/production.php - - $publisher->retainPattern('#A.+php$#'); // Would keep only Autoload.php - $publisher->retainPattern('#d.+php$#', APPPATH . 'Config/boot'); // Would keep everything but boot/production.php and boot/testing.php - Outputting Files ================ From 7ace2ccab301c91697ba1258ad40d1a955698c93 Mon Sep 17 00:00:00 2001 From: MGatner Date: Tue, 15 Jun 2021 13:29:46 +0000 Subject: [PATCH 031/490] Implement define() --- system/Files/FileCollection.php | 14 ++++++++-- tests/system/Files/FileCollectionTest.php | 32 +++++++++++++++++++++++ user_guide_src/source/libraries/files.rst | 31 +++++++++++++++++++--- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index bf4eeaf6b836..fcf2451d357d 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -122,7 +122,7 @@ protected static function matchFiles(array $files, string $pattern): array //-------------------------------------------------------------------- /** - * Loads the Filesystem helper and stores initial files. + * Loads the Filesystem helper and adds any initial files. * * @param string[] $files */ @@ -130,7 +130,17 @@ public function __construct(array $files = []) { helper(['filesystem']); - $this->set($files); + $this->add($files)->define(); + } + + /** + * Applies any initial inputs after the constructor. + * This method is a stub to be implemented by child classes. + * + * @return void + */ + protected function define(): void + { } /** diff --git a/tests/system/Files/FileCollectionTest.php b/tests/system/Files/FileCollectionTest.php index 3e77941d5d2f..a650bc464b01 100644 --- a/tests/system/Files/FileCollectionTest.php +++ b/tests/system/Files/FileCollectionTest.php @@ -99,6 +99,38 @@ public function testResolveFileDirectory() //-------------------------------------------------------------------- + public function testConstructorAddsFiles() + { + $expected = [ + $this->directory . 'apple.php', + $this->file, + ]; + + $collection = new class([$this->file]) extends FileCollection { + + protected $files = [ + SUPPORTPATH . 'Files/able/apple.php', + ]; + }; + + $this->assertSame($expected, $collection->get()); + } + + public function testConstructorCallsDefine() + { + $collection = new class([$this->file]) extends FileCollection { + + protected function define(): void + { + $this->add(SUPPORTPATH . 'Files/baker/banana.php'); + } + }; + + $this->assertSame([$this->file], $collection->get()); + } + + //-------------------------------------------------------------------- + public function testAddStringFile() { $files = new FileCollection(); diff --git a/user_guide_src/source/libraries/files.rst b/user_guide_src/source/libraries/files.rst index ad2c8671a4d5..68950b9a3d5f 100644 --- a/user_guide_src/source/libraries/files.rst +++ b/user_guide_src/source/libraries/files.rst @@ -144,12 +144,37 @@ When your collection is complete, you can use ``get()`` to retrieve the final li Below are the specific methods for working with a ``FileCollection``. -Inputting Files -=============== +Starting a Collection +===================== + +**__construct(string[] $files = [])** + +The constructor accepts an optional array of file paths to use as the initial collection. These are passed to +**add()** so any files supplied by child classes in the **$files** will remain. + +**define()** + +Allows child classes to define their own initial files. This method is called by the constructor and allows +predefined collections without having to use their methods. Example:: + + class ConfigCollection extends \CodeIgniter\Files\FileCollection + { + protected function define(): void { + + $this->add(APPPATH . 'Config', true)->retainPattern('*.php'); + } + } + +Now you may use the ``ConfigCollection`` anywhere in your project to access all App Config files without +having to re-call the collection methods every time. **set(array $files)** -Sets the list of input files to the provided string array of file paths. +Sets the list of input files to the provided string array of file paths. This will remove any existing +files from the collection, so ``$collection->set([])`` is essentially a hard reset. + +Inputting Files +=============== **add(string[]|string $paths, bool $recursive = true)** From e19f3928f3d43c03d3cf63b47b1bb4b33a5aa1ae Mon Sep 17 00:00:00 2001 From: MGatner Date: Tue, 15 Jun 2021 13:43:37 +0000 Subject: [PATCH 032/490] Declare final methods --- system/Files/FileCollection.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index fcf2451d357d..f7e7c67324c7 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -45,7 +45,7 @@ class FileCollection implements Countable, IteratorAggregate * * @throws FileException */ - protected static function resolveDirectory(string $directory): string + final protected static function resolveDirectory(string $directory): string { if (! is_dir($directory = set_realpath($directory))) { @@ -65,7 +65,7 @@ protected static function resolveDirectory(string $directory): string * * @throws FileException */ - protected static function resolveFile(string $file): string + final protected static function resolveFile(string $file): string { if (! is_file($file = set_realpath($file))) { @@ -84,7 +84,7 @@ protected static function resolveFile(string $file): string * * @return string[] */ - protected static function filterFiles(array $files, string $directory): array + final protected static function filterFiles(array $files, string $directory): array { $directory = self::resolveDirectory($directory); @@ -101,7 +101,7 @@ protected static function filterFiles(array $files, string $directory): array * * @return string[] */ - protected static function matchFiles(array $files, string $pattern): array + final protected static function matchFiles(array $files, string $pattern): array { // Convert pseudo-regex into their true form if (@preg_match($pattern, null) === false) // @phpstan-ignore-line From 096b5bdd5f11a69f35c35010f89cc77182c2c294 Mon Sep 17 00:00:00 2001 From: MGatner Date: Tue, 15 Jun 2021 11:12:53 -0400 Subject: [PATCH 033/490] Apply suggestions from code review Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com> --- system/Files/FileCollection.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index f7e7c67324c7..4231c5776642 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -88,7 +88,7 @@ final protected static function filterFiles(array $files, string $directory): ar { $directory = self::resolveDirectory($directory); - return array_filter($files, function ($value) use ($directory) { + return array_filter($files, static function (string $value) use ($directory): bool { return strpos($value, $directory) === 0; }); } @@ -114,7 +114,7 @@ final protected static function matchFiles(array $files, string $pattern): array $pattern = "#{$pattern}#"; } - return array_filter($files, function ($value) use ($pattern) { + return array_filter($files, static function ($value) use ($pattern) { return (bool) preg_match($pattern, basename($value)); }); } @@ -150,7 +150,7 @@ protected function define(): void */ public function get(): array { - $this->files = array_unique($this->files, SORT_STRING); + $this->files = array_unique($this->files); sort($this->files, SORT_STRING); return $this->files; From d1408104b967840efd4b2e889f1570bcf71630e2 Mon Sep 17 00:00:00 2001 From: MGatner Date: Wed, 23 Jun 2021 13:47:27 +0000 Subject: [PATCH 034/490] Implement review suggestions --- system/Files/FileCollection.php | 14 +++++++++++++- system/Publisher/Publisher.php | 14 ++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index 4231c5776642..cc11cf8a7962 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -34,6 +34,8 @@ class FileCollection implements Countable, IteratorAggregate */ protected $files = []; + //-------------------------------------------------------------------- + // Support Methods //-------------------------------------------------------------------- /** @@ -104,7 +106,7 @@ final protected static function filterFiles(array $files, string $directory): ar final protected static function matchFiles(array $files, string $pattern): array { // Convert pseudo-regex into their true form - if (@preg_match($pattern, null) === false) // @phpstan-ignore-line + if (@preg_match($pattern, '') === false) { $pattern = str_replace( ['#', '.', '*', '?'], @@ -119,6 +121,8 @@ final protected static function matchFiles(array $files, string $pattern): array }); } + //-------------------------------------------------------------------- + // Class Core //-------------------------------------------------------------------- /** @@ -209,6 +213,8 @@ public function add($paths, bool $recursive = true) return $this; } + //-------------------------------------------------------------------- + // File Handling //-------------------------------------------------------------------- /** @@ -268,6 +274,8 @@ public function removeFile(string $file) return $this->removeFiles([$file]); } + //-------------------------------------------------------------------- + // Directory Handling //-------------------------------------------------------------------- /** @@ -317,6 +325,8 @@ public function addDirectory(string $directory, bool $recursive = false) return $this; } + //-------------------------------------------------------------------- + // Filtering //-------------------------------------------------------------------- /** @@ -366,6 +376,8 @@ public function retainPattern(string $pattern, string $scope = null) } //-------------------------------------------------------------------- + // Interface Methods + //-------------------------------------------------------------------- /** * Returns the current number of files in the collection. diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index 1f28bc331baf..ab4484cdea0b 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -87,6 +87,8 @@ class Publisher extends FileCollection */ protected $destination = FCPATH; + //-------------------------------------------------------------------- + // Support Methods //-------------------------------------------------------------------- /** @@ -154,6 +156,8 @@ private static function wipeDirectory(string $directory): void } } + //-------------------------------------------------------------------- + // Class Core //-------------------------------------------------------------------- /** @@ -217,6 +221,8 @@ public function publish(): bool return $this->addPath('/')->merge(true); } + //-------------------------------------------------------------------- + // Property Accessors //-------------------------------------------------------------------- /** @@ -275,6 +281,8 @@ final public function getPublished(): array return $this->published; } + //-------------------------------------------------------------------- + // Additional Handlers //-------------------------------------------------------------------- /** @@ -310,8 +318,6 @@ final public function addPath(string $path, bool $recursive = true) return $this; } - //-------------------------------------------------------------------- - /** * Downloads and stages files from an array of URIs. * @@ -347,6 +353,8 @@ final public function addUri(string $uri) return $this->addFile($file); } + //-------------------------------------------------------------------- + // Write Methods //-------------------------------------------------------------------- /** @@ -361,8 +369,6 @@ final public function wipe() return $this; } - //-------------------------------------------------------------------- - /** * Copies all files into the destination, does not create directory structure. * From a6d10631092a066d1965afebb9bcb316f842abb7 Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 24 Jun 2021 07:57:49 -0400 Subject: [PATCH 035/490] Update system/Files/FileCollection.php Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com> --- system/Files/FileCollection.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index cc11cf8a7962..128d884940d5 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -185,10 +185,7 @@ public function set(array $files) */ public function add($paths, bool $recursive = true) { - if (! is_array($paths)) - { - $paths = [$paths]; - } + $paths = (array) $paths; foreach ($paths as $path) { From 28bf8467495d9d66eace5bd647bb120b625af53c Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 24 Jun 2021 07:58:26 -0400 Subject: [PATCH 036/490] Update user_guide_src/source/libraries/files.rst Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com> --- user_guide_src/source/libraries/files.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/libraries/files.rst b/user_guide_src/source/libraries/files.rst index 68950b9a3d5f..ba52138052f5 100644 --- a/user_guide_src/source/libraries/files.rst +++ b/user_guide_src/source/libraries/files.rst @@ -159,8 +159,8 @@ predefined collections without having to use their methods. Example:: class ConfigCollection extends \CodeIgniter\Files\FileCollection { - protected function define(): void { - + protected function define(): void + { $this->add(APPPATH . 'Config', true)->retainPattern('*.php'); } } From 8d5aca773b88ecf818310aeda4462dc544d7189f Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 24 Jun 2021 07:59:38 -0400 Subject: [PATCH 037/490] Update tests/system/Files/FileCollectionTest.php --- tests/system/Files/FileCollectionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Files/FileCollectionTest.php b/tests/system/Files/FileCollectionTest.php index a650bc464b01..ae2df9178f5f 100644 --- a/tests/system/Files/FileCollectionTest.php +++ b/tests/system/Files/FileCollectionTest.php @@ -118,7 +118,7 @@ public function testConstructorAddsFiles() public function testConstructorCallsDefine() { - $collection = new class([$this->file]) extends FileCollection { + $collection = new class() extends FileCollection { protected function define(): void { From 22aa7d22d421cb78ab6683de1fa14c5e2e79960a Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 27 Jun 2021 07:42:27 +0200 Subject: [PATCH 038/490] Update docblock for redirect function [ci skip] --- system/Common.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/system/Common.php b/system/Common.php index 4dfcd2cee170..bacac6ac0e4b 100644 --- a/system/Common.php +++ b/system/Common.php @@ -811,9 +811,7 @@ function old(string $key, $default = null, $escape = 'html') /** * Convenience method that works with the current global $request and * $router instances to redirect using named/reverse-routed routes - * to determine the URL to go to. If nothing is found, will treat - * as a traditional redirect and pass the string in, letting - * $response->redirect() determine the correct method and code. + * to determine the URL to go to. * * If more control is needed, you must use $response->redirect explicitly. * From bde51819f3797727ad24d8c0c47078c17787a829 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 27 Jun 2021 08:30:52 +0200 Subject: [PATCH 039/490] Fix mysqli ssl connection - certificate is not required to establish a secure connection --- system/Database/MySQLi/Connection.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 78652c25b3f3..965d539be878 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -156,7 +156,6 @@ public function connect(bool $persistent = false) } } - $clientFlags += MYSQLI_CLIENT_SSL; $this->mysqli->ssl_set( $ssl['key'] ?? null, $ssl['cert'] ?? null, @@ -165,6 +164,8 @@ public function connect(bool $persistent = false) $ssl['cipher'] ?? null ); } + + $clientFlags += MYSQLI_CLIENT_SSL; } try { From 1db19c952676aa772ab50670aea967484ec6a6f9 Mon Sep 17 00:00:00 2001 From: Alex Schmitz Date: Mon, 28 Jun 2021 09:02:59 +0200 Subject: [PATCH 040/490] Structure debug toolbar timeline with collapsible elements. --- system/Debug/Toolbar.php | 92 +++++++++++++++++++++++--- system/Debug/Toolbar/Views/toolbar.css | 25 ++++++- system/Debug/Toolbar/Views/toolbar.js | 22 ++++++ 3 files changed, 127 insertions(+), 12 deletions(-) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 32e1f951a3df..6a5ece61a373 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -149,7 +149,7 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques $data['vars']['response'] = [ 'statusCode' => $response->getStatusCode(), - 'reason' => esc($response->getReason()), + 'reason' => esc($response->getReasonPhrase()), 'contentType' => esc($response->getHeaderLine('content-type')), ]; @@ -166,17 +166,38 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques * Called within the view to display the timeline itself. */ protected function renderTimeline(array $collectors, float $startTime, int $segmentCount, int $segmentDuration, array &$styles): string + { + $rows = $this->collectTimelineData($collectors); + $output = ''; + $styleCount = 0; + + // Use recursive render function + return $this->renderTimelineRecursive($rows, $startTime, $segmentCount, $segmentDuration, $styles, $styleCount); + } + + /** + * Recursively renders timeline elements and their children. + */ + protected function renderTimelineRecursive(array $rows, float $startTime, int $segmentCount, int $segmentDuration, array &$styles, int &$styleCount, int $level = 0, bool $isChild = false): string { $displayTime = $segmentCount * $segmentDuration; - $rows = $this->collectTimelineData($collectors); - $output = ''; - $styleCount = 0; + + $output = ''; foreach ($rows as $row) { - $output .= ''; - $output .= "{$row['name']}"; - $output .= "{$row['component']}"; - $output .= "" . number_format($row['duration'] * 1000, 2) . ' ms'; + $hasChildren = isset($row['children']) && ! empty($row['children']); + + $open = $row['name'] === 'Controller'; + + if ($hasChildren) { + $output .= ''; + } else { + $output .= ''; + } + + $output .= '' . ($hasChildren ? '' : '') . $row['name'] . ''; + $output .= '' . $row['component'] . ''; + $output .= '' . number_format($row['duration'] * 1000, 2) . ' ms'; $output .= ""; $offset = ((((float) $row['start'] - $startTime) * 1000) / $displayTime) * 100; @@ -189,6 +210,19 @@ protected function renderTimeline(array $collectors, float $startTime, int $segm $output .= ''; $styleCount++; + + // Add children if any + if ($hasChildren) { + $output .= ''; + $output .= ''; + $output .= ''; + $output .= ''; + $output .= $this->renderTimelineRecursive($row['children'], $startTime, $segmentCount, $segmentDuration, $styles, $styleCount, $level + 1, true); + $output .= ''; + $output .= '
'; + $output .= ''; + $output .= ''; + } } return $output; @@ -214,15 +248,51 @@ protected function collectTimelineData($collectors): array // Sort it $sortArray = [ - array_column($data, 'start'), SORT_NUMERIC, SORT_ASC, - array_column($data, 'duration'), SORT_NUMERIC, SORT_DESC, - &$data + array_column($data, 'start'), SORT_NUMERIC, SORT_ASC, + array_column($data, 'duration'), SORT_NUMERIC, SORT_DESC, + &$data, ]; + array_multisort(...$sortArray); + // Add end time to each element + array_walk($data, static function (&$row) { + $row['end'] = $row['start'] + $row['duration']; + }); + + // Group it + $data = $this->structureTimelineData($data); + return $data; } + /** + * Arranges the already sorted timeline data into a parent => child structure. + */ + protected function structureTimelineData(array $elements): array + { + // We define ourselves as the first element of the array + $element = array_shift($elements); + + // If we have children behind us, collect and attach them to us + while (! empty($elements) && $elements[array_key_first($elements)]['end'] <= $element['end']) { + $element['children'][] = array_shift($elements); + } + + // Make sure our children know whether they have children, too + if (isset($element['children'])) { + $element['children'] = $this->structureTimelineData($element['children']); + } + + // If we have no younger siblings, we can return + if (empty($elements)) { + return [$element]; + } + + // Make sure our younger siblings know their relatives, too + return array_merge([$element], $this->structureTimelineData($elements)); + } + /** * Returns an array of data from all of the modules * that should be displayed in the 'Vars' tab. diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index f881dd18d6ec..bc8b4ac44ef7 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -186,6 +186,22 @@ #debug-bar .timeline { margin-left: 0; width: 100%; } + #debug-bar .timeline tr.timeline-parent { + cursor: pointer; } + #debug-bar .timeline tr.timeline-parent td:first-child nav { + background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent; + background-position: 0 25%; + display: inline-block; + height: 15px; + width: 15px; + margin-right: 3px; + vertical-align: middle; } + #debug-bar .timeline tr.timeline-parent.timeline-parent-open td:first-child nav { + background-position: 0 75%; } + #debug-bar .timeline tr.timeline-parent.timeline-parent-open { + background-color: #DFDFDF; } + #debug-bar .timeline tr.child-row:hover { + background: transparent; } #debug-bar .timeline th { border-left: 1px solid; font-size: 12px; @@ -200,7 +216,14 @@ padding: 5px; position: relative; } #debug-bar .timeline td:first-child { - border-left: 0; } + border-left: 0; + max-width: none; } + #debug-bar .timeline td.child-container { + padding: 0px; } + #debug-bar .timeline td.child-container .timeline{ + margin: 0px; } + #debug-bar .timeline td.child-container td:first-child:not(.child-container){ + padding-left: calc(5px + 10px * var(--level)); } #debug-bar .timeline .timer { border-radius: 4px; -moz-border-radius: 4px; diff --git a/system/Debug/Toolbar/Views/toolbar.js b/system/Debug/Toolbar/Views/toolbar.js index 563cf21cd682..690535f2de0d 100644 --- a/system/Debug/Toolbar/Views/toolbar.js +++ b/system/Debug/Toolbar/Views/toolbar.js @@ -141,6 +141,28 @@ var ciDebugBar = { } }, + /** + * Toggle display of timeline child elements + * + * @param obj + */ + toggleChildRows : function (obj) { + if (typeof obj == 'string') + { + par = document.getElementById(obj + '_parent') + obj = document.getElementById(obj + '_children'); + } + + if (par && obj) + { + obj.style.display = obj.style.display == 'none' ? '' : 'none'; + par.classList.toggle('timeline-parent-open'); + } + }, + + + //-------------------------------------------------------------------- + /** * Toggle tool bar from full to icon and icon to full */ From bd15c27f935c8704cb4b9a74bdeb50ec51c4bf9d Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Tue, 29 Jun 2021 23:28:18 -0500 Subject: [PATCH 041/490] Updated Query Build custom string option for where to remove make it clear the values do not get escaped. --- user_guide_src/source/database/queries.rst | 1 + user_guide_src/source/database/query_builder.rst | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/database/queries.rst b/user_guide_src/source/database/queries.rst index 366b264cc45c..af15bacea038 100644 --- a/user_guide_src/source/database/queries.rst +++ b/user_guide_src/source/database/queries.rst @@ -15,6 +15,7 @@ Regular Queries To submit a query, use the **query** function:: + $db = db_connect(); $db->query('YOUR QUERY HERE'); The ``query()`` function returns a database result **object** when "read" diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 78c1b3731fd5..342da67461e3 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -244,7 +244,10 @@ This function enables you to set **WHERE** clauses using one of four methods: .. note:: All values passed to this function are escaped automatically, - producing safer queries. + producing safer queries, except when using a custom string. + +.. note:: ``$builder->where()`` accepts an optional third parameter. If you set it to + ``false``, CodeIgniter will not try to protect your field or table names. #. **Simple key/value method:** @@ -294,15 +297,18 @@ methods: #. **Custom string:** You can write your own clauses manually:: + $where = "name='Joe' AND status='boss' OR status='active'"; $builder->where($where); - ``$builder->where()`` accepts an optional third parameter. If you set it to - ``false``, CodeIgniter will not try to protect your field or table names. + If you are using user-supplied data within the string, you MUST escape the + data manually. Failure to do so could result in SQL injections. +:: - :: + $name = $builder->db->escape('Joe'); + $where = "name={$name} AND status='boss' OR status='active'"; + $builder->where($where); - $builder->where('MATCH (field) AGAINST ("value")', null, false); #. **Subqueries:** You can use an anonymous function to create a subquery. From 61c01ed40bc3231c61033f31134b8f4f1581cc22 Mon Sep 17 00:00:00 2001 From: Ferenc Date: Fri, 2 Jul 2021 19:13:54 +0100 Subject: [PATCH 042/490] Finalize SQLSRV schema support for 4.2 --- system/Database/SQLSRV/Builder.php | 98 +++++++++++++++++-- system/Database/SQLSRV/Connection.php | 2 +- system/Database/SQLSRV/Forge.php | 92 +++++++++++++---- system/Database/SQLSRV/Utils.php | 8 ++ .../Database/Live/PreparedQueryTest.php | 6 +- .../Migrations/MigrationRunnerTest.php | 5 +- 6 files changed, 181 insertions(+), 30 deletions(-) diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index cda4fac8a825..d4fe5a85b540 100755 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -170,6 +170,16 @@ protected function _insert(string $table, array $keys, array $unescapedKeys): st return $this->keyPermission ? $this->addIdentity($fullTableName, $statement) : $statement; } + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + */ + protected function _insertBatch(string $table, array $keys, array $values): string + { + return 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $this->getFullName($table) . ' (' . implode(', ', $keys) . ') VALUES ' . implode(', ', $values); + } + /** * Generates a platform-specific update string from the supplied data */ @@ -183,12 +193,48 @@ protected function _update(string $table, array $values): string $fullTableName = $this->getFullName($table); - $statement = 'UPDATE ' . (empty($this->QBLimit) ? '' : 'TOP(' . $this->QBLimit . ') ') . $fullTableName . ' SET ' - . implode(', ', $valstr) . $this->compileWhereHaving('QBWhere') . $this->compileOrderBy(); + $statement = sprintf('UPDATE %s%s SET ', empty($this->QBLimit) ? '' : 'TOP(' . $this->QBLimit . ') ', $fullTableName); + + $statement .= implode(', ', $valstr) + . $this->compileWhereHaving('QBWhere') + . $this->compileOrderBy(); return $this->keyPermission ? $this->addIdentity($fullTableName, $statement) : $statement; } + /** + * Update_Batch statement + * + * Generates a platform-specific batch update string from the supplied data + */ + protected function _updateBatch(string $table, array $values, string $index): string + { + $ids = []; + $final = []; + + foreach ($values as $val) { + $ids[] = $val[$index]; + + foreach (array_keys($val) as $field) { + if ($field !== $index) { + $final[$field][] = 'WHEN ' . $index . ' = ' . $val[$index] . ' THEN ' . $val[$field]; + } + } + } + + $cases = ''; + + foreach ($final as $k => $v) { + $cases .= $k . " = CASE \n" + . implode("\n", $v) . "\n" + . 'ELSE ' . $k . ' END, '; + } + + $this->where($index . ' IN(' . implode(',', $ids) . ')', null, false); + + return 'UPDATE ' . $this->compileIgnore('update') . ' ' . $this->getFullName($table) . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere'); + } + /** * Increments a numeric column by the specified value. * @@ -203,6 +249,7 @@ public function increment(string $column, int $value = 1) } else { $values = [$column => "{$column} + {$value}"]; } + $sql = $this->_update($this->QBFrom[0], $values); return $this->db->query($sql, $this->binds, false); @@ -222,6 +269,7 @@ public function decrement(string $column, int $value = 1) } else { $values = [$column => "{$column} + {$value}"]; } + $sql = $this->_update($this->QBFrom[0], $values); return $this->db->query($sql, $this->binds, false); @@ -304,9 +352,10 @@ public function replace(?array $set = null) return $sql; } - $this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->escapeIdentifiers($table) . ' ON'); + $this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->getFullName($table) . ' ON'); + $result = $this->db->query($sql, $this->binds, false); - $this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->escapeIdentifiers($table) . ' OFF'); + $this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->getFullName($table) . ' OFF'); return $result; } @@ -410,6 +459,40 @@ protected function maxMinAvgSum(string $select = '', string $alias = '', string return $this; } + /** + * "Count All" query + * + * Generates a platform-specific query string that counts all records in + * the particular table + * + * @param bool $reset Are we want to clear query builder values? + * + * @return int|string when $test = true + */ + public function countAll(bool $reset = true) + { + $table = $this->QBFrom[0]; + + $sql = $this->countString . $this->db->escapeIdentifiers('numrows') . ' FROM ' . $this->getFullName($table); + + if ($this->testMode) { + return $sql; + } + + $query = $this->db->query($sql, null, false); + if (empty($query->getResult())) { + return 0; + } + + $query = $query->getRow(); + + if ($reset === true) { + $this->resetSelect(); + } + + return (int) $query->numrows; + } + /** * Delete statement */ @@ -504,9 +587,10 @@ protected function compileSelect($selectOverride = false): string } $sql .= $this->compileWhereHaving('QBWhere') - . $this->compileGroupBy() - . $this->compileWhereHaving('QBHaving') - . $this->compileOrderBy(); // ORDER BY + . $this->compileGroupBy() + . $this->compileWhereHaving('QBHaving') + . $this->compileOrderBy(); // ORDER BY + // LIMIT if ($this->QBLimit) { $sql = $this->_limit($sql . "\n"); diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index f09a96d69213..3c86839d2e88 100755 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -215,7 +215,7 @@ protected function _listColumns(string $table = ''): string */ protected function _indexData(string $table): array { - $sql = 'EXEC sp_helpindex ' . $this->escape($table); + $sql = 'EXEC sp_helpindex ' . $this->escape($this->schema . '.' . $table); if (($query = $this->query($sql)) === false) { throw new DatabaseException(lang('Database.failGetIndexData')); diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 43131698703f..aa646ccf2d6f 100755 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Database\SQLSRV; +use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Forge as BaseForge; /** @@ -23,7 +24,7 @@ class Forge extends BaseForge * * @var string */ - protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s'; + protected $dropConstraintStr; /** * CREATE DATABASE IF statement @@ -61,7 +62,7 @@ class Forge extends BaseForge * * @var string */ - protected $renameTableStr = 'EXEC sp_rename %s , %s ;'; + protected $renameTableStr; /** * UNSIGNED support @@ -80,21 +81,32 @@ class Forge extends BaseForge * * @var string */ - protected $createTableIfStr = "IF NOT EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nCREATE TABLE"; + protected $createTableIfStr; /** * CREATE TABLE statement * * @var string */ - protected $createTableStr = "%s %s (%s\n) "; + protected $createTableStr; - /** - * DROP TABLE IF statement - * - * @var string - */ - protected $_drop_table_if = "IF EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nDROP TABLE"; + public function __construct(BaseConnection $db) + { + parent::__construct($db); + + $this->createTableIfStr = 'IF NOT EXISTS' + . '(SELECT t.name, s.name as schema_name, t.type_desc ' + . 'FROM sys.tables t ' + . 'INNER JOIN sys.schemas s on s.schema_id = t.schema_id ' + . "WHERE s.name=N'" . $this->db->schema . "' " + . "AND t.name=REPLACE(N'%s', '\"', '') " + . "AND t.type_desc='USER_TABLE')\nCREATE TABLE "; + + $this->createTableStr = '%s ' . $this->db->escapeIdentifiers($this->db->schema) . ".%s (%s\n) "; + $this->renameTableStr = 'EXEC sp_rename [' . $this->db->escapeIdentifiers($this->db->schema) . '.%s] , %s ;'; + + $this->dropConstraintStr = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.%s DROP CONSTRAINT %s'; + } /** * CREATE TABLE attributes @@ -111,9 +123,6 @@ protected function _createTableAttributes(array $attributes): string */ protected function _alterTable(string $alterType, string $table, $field) { - if ($alterType === 'ADD') { - return parent::_alterTable($alterType, $table, $field); - } // Handle DROP here if ($alterType === 'DROP') { @@ -133,7 +142,7 @@ protected function _alterTable(string $alterType, string $table, $field) } } - $sql = 'ALTER TABLE [' . $table . '] DROP '; + $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table) . ' DROP '; $fields = array_map(static function ($item) { return 'COLUMN [' . trim($item) . ']'; @@ -142,10 +151,19 @@ protected function _alterTable(string $alterType, string $table, $field) return $sql . implode(',', $fields); } - $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table); + $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table); + $sql .= ($alterType === 'ADD') ? 'ADD ' : ' '; $sqls = []; + if ($alterType === 'ADD') { + foreach ($field as $data) { + $sqls[] = $sql . ($data['_literal'] !== false ? $data['_literal'] : $this->_processColumn($data)); + } + + return $sqls; + } + foreach ($field as $data) { if ($data['_literal'] !== false) { return false; @@ -198,6 +216,46 @@ protected function _dropIndex(string $table, object $indexData) return $this->db->simpleQuery($sql); } + /** + * Process indexes + * + * @return array|string + */ + protected function _processIndexes(string $table) + { + $sqls = []; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) { + $this->keys[$i] = (array) $this->keys[$i]; + + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) { + if (! isset($this->fields[$this->keys[$i][$i2]])) { + unset($this->keys[$i][$i2]); + } + } + + if (count($this->keys[$i]) <= 0) { + continue; + } + + if (in_array($i, $this->uniqueKeys, true)) { + $sqls[] = 'ALTER TABLE ' + . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table) + . ' ADD CONSTRAINT ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i])) + . ' UNIQUE (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');'; + + continue; + } + + $sqls[] = 'CREATE INDEX ' + . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i])) + . ' ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table) + . ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');'; + } + + return $sqls; + } + /** * Process column */ @@ -229,7 +287,7 @@ protected function _processForeignKeys(string $table): string $sql .= ",\n\t CONSTRAINT " . $this->db->escapeIdentifiers($nameIndex) . ' FOREIGN KEY (' . $this->db->escapeIdentifiers($field) . ') ' - . ' REFERENCES ' . $this->db->escapeIdentifiers($this->db->getPrefix() . $fkey['table']) . ' (' . $this->db->escapeIdentifiers($fkey['field']) . ')'; + . ' REFERENCES ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($this->db->getPrefix() . $fkey['table']) . ' (' . $this->db->escapeIdentifiers($fkey['field']) . ')'; if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { $sql .= ' ON DELETE ' . $fkey['onDelete']; @@ -245,8 +303,6 @@ protected function _processForeignKeys(string $table): string /** * Process primary keys - * - * @param string $table Table name */ protected function _processPrimaryKeys(string $table): string { diff --git a/system/Database/SQLSRV/Utils.php b/system/Database/SQLSRV/Utils.php index da4ac8679b9e..cf94d3dad783 100755 --- a/system/Database/SQLSRV/Utils.php +++ b/system/Database/SQLSRV/Utils.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Database\SQLSRV; use CodeIgniter\Database\BaseUtils; +use CodeIgniter\Database\ConnectionInterface; use CodeIgniter\Database\Exceptions\DatabaseException; /** @@ -33,6 +34,13 @@ class Utils extends BaseUtils */ protected $optimizeTable = 'ALTER INDEX all ON %s REORGANIZE'; + public function __construct(ConnectionInterface &$db) + { + parent::__construct($db); + + $this->optimizeTable = 'ALTER INDEX all ON ' . $this->db->schema . '.%s REORGANIZE'; + } + /** * Platform dependent version of the backup function. * diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index b71c0498f80e..06999ea93f4c 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -50,7 +50,7 @@ public function testPrepareReturnsPreparedQuery() if ($this->db->DBDriver === 'SQLSRV') { $database = $this->db->getDatabase(); - $expected = "INSERT INTO {$ec}{$database}{$ec}.{$ec}dbo{$ec}.{$ec}{$pre}user{$ec} ({$ec}name{$ec},{$ec}email{$ec}) VALUES ({$placeholders})"; + $expected = "INSERT INTO {$ec}{$database}{$ec}.{$ec}{$this->db->schema}{$ec}.{$ec}{$pre}user{$ec} ({$ec}name{$ec},{$ec}email{$ec}) VALUES ({$placeholders})"; } else { $expected = "INSERT INTO {$ec}{$pre}user{$ec} ({$ec}name{$ec}, {$ec}email{$ec}) VALUES ({$placeholders})"; } @@ -107,6 +107,10 @@ public function testExecuteRunsQueryAndReturnsManualResultObject() $query = $this->db->prepare(static function ($db) { $sql = "INSERT INTO {$db->DBPrefix}user (name, email, country) VALUES (?, ?, ?)"; + if ($db->DBDriver === 'SQLSRV') { + $sql = "INSERT INTO {$db->schema}.{$db->DBPrefix}user (name, email, country) VALUES (?, ?, ?)"; + } + return (new Query($db))->setQuery($sql); }); diff --git a/tests/system/Database/Migrations/MigrationRunnerTest.php b/tests/system/Database/Migrations/MigrationRunnerTest.php index 9dee57a3434e..fa867c7e1f0e 100644 --- a/tests/system/Database/Migrations/MigrationRunnerTest.php +++ b/tests/system/Database/Migrations/MigrationRunnerTest.php @@ -93,7 +93,7 @@ public function testGetHistory() ]; if ($this->db->DBDriver === 'SQLSRV') { - $this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->prefixTable('migrations') . ' ON'); + $this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->prefixTable('migrations') . ' ON'); } $this->hasInDatabase('migrations', $expected); @@ -110,8 +110,7 @@ public function testGetHistory() $this->assertSame($expected, $history); if ($this->db->DBDriver === 'SQLSRV') { - $this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->prefixTable('migrations') . ' OFF'); - + $this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->prefixTable('migrations') . ' OFF'); $db = $this->getPrivateProperty($runner, 'db'); $db->table('migrations')->delete(['id' => 4]); } From 906daf8a1d92a910a8d997d26f2f8508a8875ca6 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 4 Jul 2021 10:38:37 +0200 Subject: [PATCH 043/490] Update Throttler docs [ci skip] --- user_guide_src/source/libraries/throttler.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/libraries/throttler.rst b/user_guide_src/source/libraries/throttler.rst index 22d7829899cd..298825acf8f2 100644 --- a/user_guide_src/source/libraries/throttler.rst +++ b/user_guide_src/source/libraries/throttler.rst @@ -75,10 +75,9 @@ along the lines of:: { $throttler = Services::throttler(); - // Restrict an IP address to no more - // than 1 request per second across the - // entire site. - if ($throttler->check($request->getIPAddress(), 60, MINUTE) === false) { + // Restrict an IP address to no more than 1 request + // per second across the entire site. + if ($throttler->check(md5($request->getIPAddress()), 60, MINUTE) === false) { return Services::response()->setStatusCode(429); } } From 03d778c864666f4d2d2361c3a4eb2bf33daed54e Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 4 Jul 2021 11:34:23 +0200 Subject: [PATCH 044/490] Fix getVar method when trying to get variable that does not exists --- system/HTTP/IncomingRequest.php | 4 ++++ tests/system/HTTP/IncomingRequestTest.php | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 4dec7ebc610f..21894949cb29 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -535,6 +535,10 @@ public function getJsonVar(string $index, bool $assoc = false, ?int $filter = nu $data = dot_array_search($index, $this->getJSON(true)); + if ($data === null) { + return null; + } + if (! is_array($data)) { $filter = $filter ?? FILTER_DEFAULT; $flags = is_array($flags) ? $flags : (is_numeric($flags) ? (int) $flags : 0); diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index a64445232aba..4e837436ef12 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -247,7 +247,7 @@ public function testNegotiatesNot() public function testNegotiatesCharset() { - // $_SERVER['HTTP_ACCEPT_CHARSET'] = 'iso-8859-5, unicode-1-1;q=0.8'; + // $_SERVER['HTTP_ACCEPT_CHARSET'] = 'iso-8859-5, unicode-1-1;q=0.8'; $this->request->setHeader('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8'); $this->assertSame(strtolower($this->request->config->charset), $this->request->negotiate('charset', ['iso-8859', 'unicode-1-2'])); @@ -299,6 +299,7 @@ public function testCanGetAVariableFromJson() $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); $this->assertSame('bar', $request->getJsonVar('foo')); + $this->assertNull($request->getJsonVar('notExists')); $jsonVar = $request->getJsonVar('baz'); $this->assertIsObject($jsonVar); $this->assertSame('buzz', $jsonVar->fizz); @@ -354,6 +355,7 @@ public function testGetVarWorksWithJson() $this->assertSame('bar', $request->getVar('foo')); $this->assertSame('buzz', $request->getVar('fizz')); + $this->assertNull($request->getVar('notExists')); $multiple = $request->getVar(['foo', 'fizz']); $this->assertIsArray($multiple); From abd07f95ec89ca8eeb691609ab207ae972741d47 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 4 Jul 2021 21:20:17 +0200 Subject: [PATCH 045/490] Update tests/system/HTTP/IncomingRequestTest.php Co-authored-by: MGatner --- tests/system/HTTP/IncomingRequestTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 4e837436ef12..6f6172af54a7 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -300,6 +300,7 @@ public function testCanGetAVariableFromJson() $this->assertSame('bar', $request->getJsonVar('foo')); $this->assertNull($request->getJsonVar('notExists')); + $jsonVar = $request->getJsonVar('baz'); $this->assertIsObject($jsonVar); $this->assertSame('buzz', $jsonVar->fizz); From 1182e065a22a079c2aff605558b64137cf0b6025 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 4 Jul 2021 21:20:23 +0200 Subject: [PATCH 046/490] Update tests/system/HTTP/IncomingRequestTest.php Co-authored-by: MGatner --- tests/system/HTTP/IncomingRequestTest.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 6f6172af54a7..1e975359a120 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -342,11 +342,7 @@ public function testGetJsonVarCanFilter() public function testGetVarWorksWithJson() { - $jsonObj = [ - 'foo' => 'bar', - 'fizz' => 'buzz', - ]; - $json = json_encode($jsonObj); + $json = json_encode(['foo' => 'bar', 'fizz' => 'buzz']); $config = new App(); $config->baseURL = 'http://example.com/'; From 57c53427c4c38e9f0a2ead4211efe4a4b6af999c Mon Sep 17 00:00:00 2001 From: Alex Schmitz Date: Tue, 6 Jul 2021 12:34:23 +0200 Subject: [PATCH 047/490] Do not overwrite startTime. --- system/CodeIgniter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 4350876bd3a2..4fd3596f94f3 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -490,7 +490,9 @@ protected function bootstrapEnvironment() */ protected function startBenchmark() { - $this->startTime = microtime(true); + if($this->startTime === null) { + $this->startTime = microtime(true); + } $this->benchmark = Services::timer(); $this->benchmark->start('total_execution', $this->startTime); From d263210eef0516d7dd44e90b9910ae32c15b8cf9 Mon Sep 17 00:00:00 2001 From: Alex Schmitz Date: Sun, 4 Jul 2021 18:22:15 +0200 Subject: [PATCH 048/490] Enable general benchmarking for filters. Fix copy paste. --- system/CodeIgniter.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 4fd3596f94f3..f18fc38384b6 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -357,7 +357,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache { $routeFilter = $this->tryToRouteIt($routes); - // Run "before" filters + // Start up the filters $filters = Services::filters(); // If any filters were specified within the routes file, @@ -371,7 +371,10 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache // Never run filters when running through Spark cli if (! defined('SPARKED')) { + // Run "before" filters + $this->benchmark->start('before_filters'); $possibleResponse = $filters->run($uri, 'before'); + $this->benchmark->stop('before_filters'); // If a ResponseInterface instance is returned then send it back to the client and stop if ($possibleResponse instanceof ResponseInterface) { @@ -410,8 +413,11 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache // Never run filters when running through Spark cli if (! defined('SPARKED')) { $filters->setResponse($this->response); + // Run "after" filters + $this->benchmark->start('after_filters'); $response = $filters->run($uri, 'after'); + $this->benchmark->stop('after_filters'); } else { $response = $this->response; From 3a6cf804692a91fe0f0c58352ad131d9d1c25bf8 Mon Sep 17 00:00:00 2001 From: Alex Schmitz Date: Mon, 5 Jul 2021 09:24:42 +0200 Subject: [PATCH 049/490] Add toolbar timer styles to sass files. --- admin/css/debug-toolbar/toolbar.scss | 53 ++++++++++++++++++++++++++ system/Debug/Toolbar/Views/toolbar.css | 36 ++++++++--------- 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/admin/css/debug-toolbar/toolbar.scss b/admin/css/debug-toolbar/toolbar.scss index 8669d0ac12ff..d06731db6ffe 100644 --- a/admin/css/debug-toolbar/toolbar.scss +++ b/admin/css/debug-toolbar/toolbar.scss @@ -326,6 +326,23 @@ &:first-child { border-left: 0; + max-width: none; + } + + &.child-container { + padding: 0px; + + .timeline { + margin: 0px; + + td { + &:first-child { + &:not(.child-container) { + padding-left: calc(5px + 10px * var(--level)); + } + } + } + } } } @@ -336,6 +353,42 @@ position: absolute; top: 30%; } + + .timeline-parent{ + cursor: pointer; + + td { + &:first-child { + nav { + background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent; + background-position: 0 25%; + display: inline-block; + height: 15px; + width: 15px; + margin-right: 3px; + vertical-align: middle; + } + } + } + } + + .timeline-parent-open { + background-color: #DFDFDF; + + td { + &:first-child { + nav { + background-position: 0 75%; + } + } + } + } + + .child-row { + &:hover { + background: transparent; + } + } } // The "Routes" tab diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index bc8b4ac44ef7..bd080de2b1c8 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -186,22 +186,6 @@ #debug-bar .timeline { margin-left: 0; width: 100%; } - #debug-bar .timeline tr.timeline-parent { - cursor: pointer; } - #debug-bar .timeline tr.timeline-parent td:first-child nav { - background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent; - background-position: 0 25%; - display: inline-block; - height: 15px; - width: 15px; - margin-right: 3px; - vertical-align: middle; } - #debug-bar .timeline tr.timeline-parent.timeline-parent-open td:first-child nav { - background-position: 0 75%; } - #debug-bar .timeline tr.timeline-parent.timeline-parent-open { - background-color: #DFDFDF; } - #debug-bar .timeline tr.child-row:hover { - background: transparent; } #debug-bar .timeline th { border-left: 1px solid; font-size: 12px; @@ -220,9 +204,9 @@ max-width: none; } #debug-bar .timeline td.child-container { padding: 0px; } - #debug-bar .timeline td.child-container .timeline{ + #debug-bar .timeline td.child-container .timeline { margin: 0px; } - #debug-bar .timeline td.child-container td:first-child:not(.child-container){ + #debug-bar .timeline td.child-container .timeline td:first-child:not(.child-container) { padding-left: calc(5px + 10px * var(--level)); } #debug-bar .timeline .timer { border-radius: 4px; @@ -232,6 +216,22 @@ padding: 5px; position: absolute; top: 30%; } + #debug-bar .timeline .timeline-parent { + cursor: pointer; } + #debug-bar .timeline .timeline-parent td:first-child nav { + background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent; + background-position: 0 25%; + display: inline-block; + height: 15px; + width: 15px; + margin-right: 3px; + vertical-align: middle; } + #debug-bar .timeline .timeline-parent-open { + background-color: #DFDFDF; } + #debug-bar .timeline .timeline-parent-open td:first-child nav { + background-position: 0 75%; } + #debug-bar .timeline .child-row:hover { + background: transparent; } #debug-bar .route-params, #debug-bar .route-params-item { vertical-align: top; } From 971f511899fde371a3bc5c6470d5cc27a8b8bca4 Mon Sep 17 00:00:00 2001 From: Alex Schmitz Date: Wed, 7 Jul 2021 13:21:55 +0200 Subject: [PATCH 050/490] Unify scss styling and switch to spaces. --- admin/css/debug-toolbar/_graphic-charter.scss | 18 +- admin/css/debug-toolbar/_mixins.scss | 13 +- admin/css/debug-toolbar/_theme-dark.scss | 257 +++--- admin/css/debug-toolbar/_theme-light.scss | 250 +++--- admin/css/debug-toolbar/toolbar.scss | 838 +++++++++--------- system/Debug/Toolbar/Views/toolbar.css | 23 +- 6 files changed, 718 insertions(+), 681 deletions(-) diff --git a/admin/css/debug-toolbar/_graphic-charter.scss b/admin/css/debug-toolbar/_graphic-charter.scss index 9e88e7177551..522f07f6e255 100644 --- a/admin/css/debug-toolbar/_graphic-charter.scss +++ b/admin/css/debug-toolbar/_graphic-charter.scss @@ -2,19 +2,19 @@ // ========================================================================== */ // Themes -$t-dark: #252525; +$t-dark: #252525; $t-light: #FFFFFF; // Glossy colors -$g-blue: #5BC0DE; -$g-gray: #434343; -$g-green: #9ACE25; +$g-blue: #5BC0DE; +$g-gray: #434343; +$g-green: #9ACE25; $g-orange: #DD8615; -$g-red: #DD4814; +$g-red: #DD4814; // Matt colors -$m-blue: #D8EAF0; -$m-gray: #DFDFDF; -$m-green: #DFF0D8; +$m-blue: #D8EAF0; +$m-gray: #DFDFDF; +$m-green: #DFF0D8; $m-orange: #FDC894; -$m-red: #EF9090; +$m-red: #EF9090; diff --git a/admin/css/debug-toolbar/_mixins.scss b/admin/css/debug-toolbar/_mixins.scss index c5bde5f5f6da..69af2b67c475 100644 --- a/admin/css/debug-toolbar/_mixins.scss +++ b/admin/css/debug-toolbar/_mixins.scss @@ -2,12 +2,13 @@ // ========================================================================== */ @mixin border-radius($radius) { - border-radius: $radius; - -moz-border-radius: $radius; - -webkit-border-radius: $radius; + border-radius: $radius; + -moz-border-radius: $radius; + -webkit-border-radius: $radius; } + @mixin box-shadow($left, $top, $radius, $color) { - box-shadow: $left $top $radius $color; - -moz-box-shadow: $left $top $radius $color; - -webkit-box-shadow: $left $top $radius $color; + box-shadow: $left $top $radius $color; + -moz-box-shadow: $left $top $radius $color; + -webkit-box-shadow: $left $top $radius $color; } diff --git a/admin/css/debug-toolbar/_theme-dark.scss b/admin/css/debug-toolbar/_theme-dark.scss index 56d4a4bbbac3..b41e8fb086af 100644 --- a/admin/css/debug-toolbar/_theme-dark.scss +++ b/admin/css/debug-toolbar/_theme-dark.scss @@ -12,11 +12,14 @@ // ========================================================================== */ #debug-icon { - background-color: $t-dark; - @include box-shadow(0, 0, 4px, $m-gray); - a:active, a:link, a:visited { - color: $g-orange; - } + background-color: $t-dark; + @include box-shadow(0, 0, 4px, $m-gray); + + a:active, + a:link, + a:visited { + color: $g-orange; + } } @@ -24,119 +27,130 @@ // ========================================================================== */ #debug-bar { - background-color: $t-dark; - color: $m-gray; - - // Reset to prevent conflict with other CSS files - h1, - h2, - h3, - p, - a, - button, - table, - thead, - tr, - td, - button, - .toolbar { - background-color: transparent; - color: $m-gray; - } - - // Buttons - button { - background-color: $t-dark; - } - - // Tables - table { - strong { - color: $m-orange; - } - tbody tr { - &:hover { - background-color: $g-gray; - } - &.current { - background-color: $m-orange; - td { - color: $t-dark; - } - &:hover td { - background-color: $g-red; - color: $t-light; - } - } - } - } - - // The toolbar - .toolbar { - background-color: $g-gray; - @include box-shadow(0, 0, 4px, $g-gray); - img { - filter: brightness(0) invert(1); - } - } - - // Fixed top - &.fixed-top { - & .toolbar { - @include box-shadow(0, 0, 4px, $g-gray); - } - .tab { - @include box-shadow(0, 1px, 4px, $g-gray); - } - } - - // "Muted" elements - .muted { - color: $m-gray; - td { - color: $g-gray; - } - &:hover td { - color: $m-gray; - } - } - - // The toolbar preferences - #toolbar-position, - #toolbar-theme, { - filter: brightness(0) invert(0.6); - } - - // The toolbar menus - .ci-label { - &.active { - background-color: $t-dark; - } - &:hover { - background-color: $t-dark; - } - .badge { - background-color: $g-blue; - color: $m-gray; - } - } - - // The tabs container - .tab { - background-color: $t-dark; - @include box-shadow(0, -1px, 4px, $g-gray); - } - - // The "Timeline" tab - .timeline { - th, - td { - border-color: $g-gray; - } - .timer { - background-color: $g-orange; - } - } + background-color: $t-dark; + color: $m-gray; + + // Reset to prevent conflict with other CSS files + h1, + h2, + h3, + p, + a, + button, + table, + thead, + tr, + td, + button, + .toolbar { + background-color: transparent; + color: $m-gray; + } + + // Buttons + button { + background-color: $t-dark; + } + + // Tables + table { + strong { + color: $m-orange; + } + + tbody tr { + &:hover { + background-color: $g-gray; + } + + &.current { + background-color: $m-orange; + + td { + color: $t-dark; + } + + &:hover td { + background-color: $g-red; + color: $t-light; + } + } + } + } + + // The toolbar + .toolbar { + background-color: $g-gray; + @include box-shadow(0, 0, 4px, $g-gray); + + img { + filter: brightness(0) invert(1); + } + } + + // Fixed top + &.fixed-top { + .toolbar { + @include box-shadow(0, 0, 4px, $g-gray); + } + + .tab { + @include box-shadow(0, 1px, 4px, $g-gray); + } + } + + // "Muted" elements + .muted { + color: $m-gray; + + td { + color: $g-gray; + } + + &:hover td { + color: $m-gray; + } + } + + // The toolbar preferences + #toolbar-position, + #toolbar-theme { + filter: brightness(0) invert(0.6); + } + + // The toolbar menus + .ci-label { + &.active { + background-color: $t-dark; + } + + &:hover { + background-color: $t-dark; + } + + .badge { + background-color: $g-blue; + color: $m-gray; + } + } + + // The tabs container + .tab { + background-color: $t-dark; + @include box-shadow(0, -1px, 4px, $g-gray); + } + + // The "Timeline" tab + .timeline { + th, + td { + border-color: $g-gray; + } + + .timer { + background-color: $g-orange; + } + } } @@ -144,9 +158,10 @@ // ========================================================================== */ .debug-view.show-view { - border-color: $g-orange; + border-color: $g-orange; } + .debug-view-path { - background-color: $m-orange; - color: $g-gray; + background-color: $m-orange; + color: $g-gray; } diff --git a/admin/css/debug-toolbar/_theme-light.scss b/admin/css/debug-toolbar/_theme-light.scss index 744997a4080d..7148cd4c4f6f 100644 --- a/admin/css/debug-toolbar/_theme-light.scss +++ b/admin/css/debug-toolbar/_theme-light.scss @@ -12,11 +12,14 @@ // ========================================================================== */ #debug-icon { - background-color: $t-light; - @include box-shadow(0, 0, 4px, $m-gray); - a:active, a:link, a:visited { - color: $g-orange; - } + background-color: $t-light; + @include box-shadow(0, 0, 4px, $m-gray); + + a:active, + a:link, + a:visited { + color: $g-orange; + } } @@ -24,116 +27,126 @@ // ========================================================================== */ #debug-bar { - background-color: $t-light; - color: $g-gray; - - // Reset to prevent conflict with other CSS files */ - h1, - h2, - h3, - p, - a, - button, - table, - thead, - tr, - td, - button, - .toolbar { - background-color: transparent; - color: $g-gray; - } - - // Buttons - button { - background-color: $t-light; - } - - // Tables - table { - strong { - color: $m-orange; - } - tbody tr { - &:hover { - background-color: $m-gray; - } - &.current { - background-color: $m-orange; - &:hover td { - background-color: $g-red; - color: $t-light; - } - } - } - } - - // The toolbar - .toolbar { - background-color: $t-light; - @include box-shadow(0, 0, 4px, $m-gray); - img { - filter: brightness(0) invert(0.4); - } - } - - // Fixed top - &.fixed-top { - & .toolbar { - @include box-shadow(0, 0, 4px, $m-gray); - } - .tab { - @include box-shadow(0, 1px, 4px, $m-gray); - } - } - - // "Muted" elements - .muted { - color: $g-gray; - td { - color: $m-gray; - } - &:hover td { - color: $g-gray; - } - } - - // The toolbar preferences - #toolbar-position, - #toolbar-theme, { - filter: brightness(0) invert(0.6); - } - - // The toolbar menus - .ci-label { - &.active { - background-color: $m-gray; - } - &:hover { - background-color: $m-gray; - } - .badge { - background-color: $g-blue; - color: $t-light; - } - } - - // The tabs container - .tab { - background-color: $t-light; - @include box-shadow(0, -1px, 4px, $m-gray); - } - - // The "Timeline" tab - .timeline { - th, - td { - border-color: $m-gray; - } - .timer { - background-color: $g-orange; - } - } + background-color: $t-light; + color: $g-gray; + + // Reset to prevent conflict with other CSS files + h1, + h2, + h3, + p, + a, + button, + table, + thead, + tr, + td, + button, + .toolbar { + background-color: transparent; + color: $g-gray; + } + + // Buttons + button { + background-color: $t-light; + } + + // Tables + table { + strong { + color: $m-orange; + } + + tbody tr { + &:hover { + background-color: $m-gray; + } + + &.current { + background-color: $m-orange; + + &:hover td { + background-color: $g-red; + color: $t-light; + } + } + } + } + + // The toolbar + .toolbar { + background-color: $t-light; + @include box-shadow(0, 0, 4px, $m-gray); + + img { + filter: brightness(0) invert(0.4); + } + } + + // Fixed top + &.fixed-top { + .toolbar { + @include box-shadow(0, 0, 4px, $m-gray); + } + + .tab { + @include box-shadow(0, 1px, 4px, $m-gray); + } + } + + // "Muted" elements + .muted { + color: $g-gray; + + td { + color: $m-gray; + } + + &:hover td { + color: $g-gray; + } + } + + // The toolbar preferences + #toolbar-position, + #toolbar-theme { + filter: brightness(0) invert(0.6); + } + + // The toolbar menus + .ci-label { + &.active { + background-color: $m-gray; + } + + &:hover { + background-color: $m-gray; + } + + .badge { + background-color: $g-blue; + color: $t-light; + } + } + + // The tabs container + .tab { + background-color: $t-light; + @include box-shadow(0, -1px, 4px, $m-gray); + } + + // The "Timeline" tab + .timeline { + th, + td { + border-color: $m-gray; + } + + .timer { + background-color: $g-orange; + } + } } @@ -141,9 +154,10 @@ // ========================================================================== */ .debug-view.show-view { - border-color: $g-orange; + border-color: $g-orange; } + .debug-view-path { - background-color: $m-orange; - color: $g-gray; + background-color: $m-orange; + color: $g-gray; } diff --git a/admin/css/debug-toolbar/toolbar.scss b/admin/css/debug-toolbar/toolbar.scss index d06731db6ffe..0f07005ad9ab 100644 --- a/admin/css/debug-toolbar/toolbar.scss +++ b/admin/css/debug-toolbar/toolbar.scss @@ -1,8 +1,8 @@ /*! CodeIgniter 4 - Debug bar * ============================================================================ - * Forum: https://forum.codeigniter.com - * Github: https://github.com/codeigniter4/codeigniter4 - * Slack: https://codeigniterchat.slack.com + * Forum: https://forum.codeigniter.com + * Github: https://github.com/codeigniter4/codeigniter4 + * Slack: https://codeigniterchat.slack.com * Website: https://codeigniter.com */ @@ -17,38 +17,38 @@ // ========================================================================== */ #debug-icon { - // Position - bottom: 0; - position: fixed; - right: 0; - z-index: 10000; - - // Size - height: 36px; - width: 36px; - - // Spacing - margin: 0px; - padding: 0px; - - // Content - clear: both; - text-align: center; - - a svg { - margin: 8px; - max-width: 20px; - max-height: 20px; - } - - &.fixed-top { - bottom: auto; - top: 0; - } - - .debug-bar-ndisplay { - display: none; - } + // Position + bottom: 0; + position: fixed; + right: 0; + z-index: 10000; + + // Size + height: 36px; + width: 36px; + + // Spacing + margin: 0px; + padding: 0px; + + // Content + clear: both; + text-align: center; + + a svg { + margin: 8px; + max-width: 20px; + max-height: 20px; + } + + &.fixed-top { + bottom: auto; + top: 0; + } + + .debug-bar-ndisplay { + display: none; + } } @@ -56,352 +56,352 @@ // ========================================================================== */ #debug-bar { - // Position - bottom: 0; - left: 0; - position: fixed; - right: 0; - z-index: 10000; - - // Size - height: 36px; - - // Spacing - line-height: 36px; - - // Typography - font-family: $base-font; - font-size: $base-size; - font-weight: 400; - - // General elements - h1 { - bottom: 0; - display: inline-block; - font-size: $base-size - 2; - font-weight: normal; - margin: 0 16px 0 0; - padding: 0; - position: absolute; - right: 30px; - text-align: left; - top: 0; - - svg { - width: 16px; - margin-right: 5px; - } - } - - h2 { - font-size: $base-size; - margin: 0; - padding: 5px 0 10px 0; - - span { - font-size: 13px; - } - } - - h3 { - font-size: $base-size - 4; - font-weight: 200; - margin: 0 0 0 10px; - padding: 0; - text-transform: uppercase; - } - - p { - font-size: $base-size - 4; - margin: 0 0 0 15px; - padding: 0; - } - - a { - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } - - button { - border: 1px solid; - @include border-radius(4px); - cursor: pointer; - line-height: 15px; - - &:hover { - text-decoration: underline; - } - } - - table { - border-collapse: collapse; - font-size: $base-size - 2; - line-height: normal; - margin: 5px 10px 15px 10px; // Tables indentation - width: calc(100% - 10px); // Make sure it still fits the container, even with the margins - - strong { - font-weight: 500; - } - - th { - display: table-cell; - font-weight: 600; - padding-bottom: 0.7em; - text-align: left; - } - - tr { - border: none; - } - - td { - border: none; - display: table-cell; - margin: 0; - text-align: left; - - &:first-child { - max-width: 20%; - - &.narrow { - width: 7em; - } - } - } - } - - td[data-debugbar-route] { - form { - display: none; - } - - &:hover { - form { - display: block; - } - - &>div { - display: none; - } - } - - input[type=text] { - padding: 2px; - } - } - - // The toolbar - .toolbar { - display: flex; - overflow: hidden; - overflow-y: auto; - padding: 0 12px 0 12px; - /* give room for OS X scrollbar */ - white-space: nowrap; - z-index: 10000; - } - - // Fixed top - &.fixed-top { - bottom: auto; - top: 0; - - .tab { - bottom: auto; - top: 36px; - } - } - - // The toolbar preferences - #toolbar-position, - #toolbar-theme { - a { - // float: left; - padding: 0 6px; - display: inline-flex; - vertical-align: top; - - &:hover { - text-decoration: none; - } - } - } - - // The "Open/Close" toggle - #debug-bar-link { - bottom: 0; - display: inline-block; - font-size: $base-size; - line-height: 36px; - padding: 6px; - position: absolute; - right: 10px; - top: 0; - width: 24px; - } - - // The toolbar menus - .ci-label { - display: inline-flex; - font-size: $base-size - 2; - // vertical-align: baseline; - - &:hover { - cursor: pointer; - } - - a { - color: inherit; - display: flex; - letter-spacing: normal; - padding: 0 10px; - text-decoration: none; - align-items: center; - } - - // The toolbar icons - img { - // clear: left; - // display: inline-block; - // float: left; - margin: 6px 3px 6px 0; - width: 16px !important; - } - - // The toolbar notification badges - .badge { - @include border-radius(12px); - display: inline-block; - font-size: 75%; - font-weight: bold; - line-height: 12px; - margin-left: 5px; - padding: 2px 5px; - text-align: center; - vertical-align: baseline; - white-space: nowrap; - } - } - - // The tabs container - .tab { - bottom: 35px; - display: none; - left: 0; - max-height: 62%; - overflow: hidden; - overflow-y: auto; - padding: 1em 2em; - position: fixed; - right: 0; - z-index: 9999; - } - - // The "Timeline" tab - .timeline { - margin-left: 0; - width: 100%; - - th { - border-left: 1px solid; - font-size: $base-size - 4; - font-weight: 200; - padding: 5px 5px 10px 5px; - position: relative; - text-align: left; - - &:first-child { - border-left: 0; - } - } - - td { - border-left: 1px solid; - padding: 5px; - position: relative; - - &:first-child { - border-left: 0; - max-width: none; - } - - &.child-container { - padding: 0px; - - .timeline { - margin: 0px; - - td { - &:first-child { - &:not(.child-container) { - padding-left: calc(5px + 10px * var(--level)); - } - } - } - } - } - } - - .timer { - @include border-radius(4px); - display: inline-block; - padding: 5px; - position: absolute; - top: 30%; - } - - .timeline-parent{ - cursor: pointer; - - td { - &:first-child { - nav { - background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent; - background-position: 0 25%; - display: inline-block; - height: 15px; - width: 15px; - margin-right: 3px; - vertical-align: middle; - } - } - } - } - - .timeline-parent-open { - background-color: #DFDFDF; - - td { - &:first-child { - nav { - background-position: 0 75%; - } - } - } - } - - .child-row { - &:hover { - background: transparent; - } - } - } - - // The "Routes" tab - .route-params, - .route-params-item { - vertical-align: top; - - td:first-child { - font-style: italic; - padding-left: 1em; - text-align: right; - } - } + // Position + bottom: 0; + left: 0; + position: fixed; + right: 0; + z-index: 10000; + + // Size + height: 36px; + + // Spacing + line-height: 36px; + + // Typography + font-family: $base-font; + font-size: $base-size; + font-weight: 400; + + // General elements + h1 { + bottom: 0; + display: inline-block; + font-size: $base-size - 2; + font-weight: normal; + margin: 0 16px 0 0; + padding: 0; + position: absolute; + right: 30px; + text-align: left; + top: 0; + + svg { + width: 16px; + margin-right: 5px; + } + } + + h2 { + font-size: $base-size; + margin: 0; + padding: 5px 0 10px 0; + + span { + font-size: 13px; + } + } + + h3 { + font-size: $base-size - 4; + font-weight: 200; + margin: 0 0 0 10px; + padding: 0; + text-transform: uppercase; + } + + p { + font-size: $base-size - 4; + margin: 0 0 0 15px; + padding: 0; + } + + a { + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + button { + border: 1px solid; + @include border-radius(4px); + cursor: pointer; + line-height: 15px; + + &:hover { + text-decoration: underline; + } + } + + table { + border-collapse: collapse; + font-size: $base-size - 2; + line-height: normal; + + // Tables indentation + margin: 5px 10px 15px 10px; + + // Make sure it still fits the container, even with the margins + width: calc(100% - 10px); + + strong { + font-weight: 500; + } + + th { + display: table-cell; + font-weight: 600; + padding-bottom: 0.7em; + text-align: left; + } + + tr { + border: none; + } + + td { + border: none; + display: table-cell; + margin: 0; + text-align: left; + + &:first-child { + max-width: 20%; + + &.narrow { + width: 7em; + } + } + } + } + + td[data-debugbar-route] { + form { + display: none; + } + + &:hover { + form { + display: block; + } + + &>div { + display: none; + } + } + + input[type=text] { + padding: 2px; + } + } + + // The toolbar + .toolbar { + display: flex; + overflow: hidden; + overflow-y: auto; + padding: 0 12px 0 12px; + + // Give room for OS X scrollbar + white-space: nowrap; + z-index: 10000; + } + + // Fixed top + &.fixed-top { + bottom: auto; + top: 0; + + .tab { + bottom: auto; + top: 36px; + } + } + + // The toolbar preferences + #toolbar-position, + #toolbar-theme { + a { + padding: 0 6px; + display: inline-flex; + vertical-align: top; + + &:hover { + text-decoration: none; + } + } + } + + // The "Open/Close" toggle + #debug-bar-link { + bottom: 0; + display: inline-block; + font-size: $base-size; + line-height: 36px; + padding: 6px; + position: absolute; + right: 10px; + top: 0; + width: 24px; + } + + // The toolbar menus + .ci-label { + display: inline-flex; + font-size: $base-size - 2; + + &:hover { + cursor: pointer; + } + + a { + color: inherit; + display: flex; + letter-spacing: normal; + padding: 0 10px; + text-decoration: none; + align-items: center; + } + + // The toolbar icons + img { + margin: 6px 3px 6px 0; + width: 16px !important; + } + + // The toolbar notification badges + .badge { + @include border-radius(12px); + display: inline-block; + font-size: 75%; + font-weight: bold; + line-height: 12px; + margin-left: 5px; + padding: 2px 5px; + text-align: center; + vertical-align: baseline; + white-space: nowrap; + } + } + + // The tabs container + .tab { + bottom: 35px; + display: none; + left: 0; + max-height: 62%; + overflow: hidden; + overflow-y: auto; + padding: 1em 2em; + position: fixed; + right: 0; + z-index: 9999; + } + + // The "Timeline" tab + .timeline { + margin-left: 0; + width: 100%; + + th { + border-left: 1px solid; + font-size: $base-size - 4; + font-weight: 200; + padding: 5px 5px 10px 5px; + position: relative; + text-align: left; + + &:first-child { + border-left: 0; + } + } + + td { + border-left: 1px solid; + padding: 5px; + position: relative; + + &:first-child { + border-left: 0; + max-width: none; + } + + &.child-container { + padding: 0px; + + .timeline { + margin: 0px; + + td { + &:first-child { + &:not(.child-container) { + padding-left: calc(5px + 10px * var(--level)); + } + } + } + } + } + } + + .timer { + @include border-radius(4px); + display: inline-block; + padding: 5px; + position: absolute; + top: 30%; + } + + .timeline-parent { + cursor: pointer; + + td { + &:first-child { + nav { + background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent; + background-position: 0 25%; + display: inline-block; + height: 15px; + width: 15px; + margin-right: 3px; + vertical-align: middle; + } + } + } + } + + .timeline-parent-open { + background-color: #DFDFDF; + + td { + &:first-child { + nav { + background-position: 0 75%; + } + } + } + } + + .child-row { + &:hover { + background: transparent; + } + } + } + + // The "Routes" tab + .route-params, + .route-params-item { + vertical-align: top; + + td:first-child { + font-style: italic; + padding-left: 1em; + text-align: right; + } + } } @@ -409,21 +409,21 @@ // ========================================================================== */ .debug-view.show-view { - border: 1px solid; - margin: 4px; + border: 1px solid; + margin: 4px; } .debug-view-path { - font-family: monospace; - font-size: $base-size - 4; - letter-spacing: normal; - min-height: 16px; - padding: 2px; - text-align: left; + font-family: monospace; + font-size: $base-size - 4; + letter-spacing: normal; + min-height: 16px; + padding: 2px; + text-align: left; } .show-view .debug-view-path { - display: block !important; + display: block !important; } @@ -431,17 +431,17 @@ // ========================================================================== */ @media screen and (max-width: 1024px) { - #debug-bar { - .ci-label { - img { - margin: unset - } - } - } - - .hide-sm { - display: none !important; - } + #debug-bar { + .ci-label { + img { + margin: unset + } + } + } + + .hide-sm { + display: none !important; + } } @@ -453,22 +453,22 @@ // If the browser supports "prefers-color-scheme" and the scheme is "Dark" @media (prefers-color-scheme: dark) { - @import '_theme-dark'; + @import '_theme-dark'; } // If we force the "Dark" theme #toolbarContainer.dark { - @import '_theme-dark'; + @import '_theme-dark'; - td[data-debugbar-route] input[type=text] { - background: #000; - color: #fff; - } + td[data-debugbar-route] input[type=text] { + background: #000; + color: #fff; + } } // If we force the "Light" theme #toolbarContainer.light { - @import '_theme-light'; + @import '_theme-light'; } @@ -476,41 +476,41 @@ // ========================================================================== */ .debug-bar-width30 { - width: 30%; + width: 30%; } .debug-bar-width10 { - width: 10%; + width: 10%; } .debug-bar-width70p { - width: 70px; + width: 70px; } .debug-bar-width140p { - width: 140px; + width: 140px; } .debug-bar-width20e { - width: 20em; + width: 20em; } .debug-bar-width6r { - width: 6rem; + width: 6rem; } .debug-bar-ndisplay { - display: none; + display: none; } .debug-bar-alignRight { - text-align: right; + text-align: right; } .debug-bar-alignLeft { - text-align: left; + text-align: left; } .debug-bar-noverflow { - overflow: hidden; -} \ No newline at end of file + overflow: hidden; +} diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index bd080de2b1c8..f42cd2089027 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -1,8 +1,8 @@ /*! CodeIgniter 4 - Debug bar * ============================================================================ - * Forum: https://forum.codeigniter.com - * Github: https://github.com/codeigniter4/codeigniter4 - * Slack: https://codeigniterchat.slack.com + * Forum: https://forum.codeigniter.com + * Github: https://github.com/codeigniter4/codeigniter4 + * Slack: https://codeigniterchat.slack.com * Website: https://codeigniter.com */ #debug-icon { @@ -117,7 +117,6 @@ overflow: hidden; overflow-y: auto; padding: 0 12px 0 12px; - /* give room for OS X scrollbar */ white-space: nowrap; z-index: 10000; } #debug-bar.fixed-top { @@ -267,7 +266,9 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #debug-icon a:active, #debug-icon a:link, #debug-icon a:visited { + #debug-icon a:active, + #debug-icon a:link, + #debug-icon a:visited { color: #DD8615; } #debug-bar { @@ -353,7 +354,9 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #debug-icon a:active, #debug-icon a:link, #debug-icon a:visited { + #debug-icon a:active, + #debug-icon a:link, + #debug-icon a:visited { color: #DD8615; } #debug-bar { background-color: #252525; @@ -437,7 +440,9 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #toolbarContainer.dark #debug-icon a:active, #toolbarContainer.dark #debug-icon a:link, #toolbarContainer.dark #debug-icon a:visited { + #toolbarContainer.dark #debug-icon a:active, + #toolbarContainer.dark #debug-icon a:link, + #toolbarContainer.dark #debug-icon a:visited { color: #DD8615; } #toolbarContainer.dark #debug-bar { @@ -528,7 +533,9 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #toolbarContainer.light #debug-icon a:active, #toolbarContainer.light #debug-icon a:link, #toolbarContainer.light #debug-icon a:visited { + #toolbarContainer.light #debug-icon a:active, + #toolbarContainer.light #debug-icon a:link, + #toolbarContainer.light #debug-icon a:visited { color: #DD8615; } #toolbarContainer.light #debug-bar { From 23227e98368483a6b1b02d60c07b712e3b41edc2 Mon Sep 17 00:00:00 2001 From: Alex Schmitz Date: Wed, 7 Jul 2021 19:41:05 +0200 Subject: [PATCH 051/490] Replace file header. --- admin/css/debug-toolbar/toolbar.scss | 13 +++++++------ system/Debug/Toolbar/Views/toolbar.css | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/admin/css/debug-toolbar/toolbar.scss b/admin/css/debug-toolbar/toolbar.scss index 0f07005ad9ab..c73510182ac7 100644 --- a/admin/css/debug-toolbar/toolbar.scss +++ b/admin/css/debug-toolbar/toolbar.scss @@ -1,9 +1,10 @@ -/*! CodeIgniter 4 - Debug bar - * ============================================================================ - * Forum: https://forum.codeigniter.com - * Github: https://github.com/codeigniter4/codeigniter4 - * Slack: https://codeigniterchat.slack.com - * Website: https://codeigniter.com +/** + * This file is part of the CodeIgniter 4 framework. + * + * (c) CodeIgniter Foundation + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ // IMPORTS diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index f42cd2089027..34b686385fd7 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -1,9 +1,10 @@ -/*! CodeIgniter 4 - Debug bar - * ============================================================================ - * Forum: https://forum.codeigniter.com - * Github: https://github.com/codeigniter4/codeigniter4 - * Slack: https://codeigniterchat.slack.com - * Website: https://codeigniter.com +/** + * This file is part of the CodeIgniter 4 framework. + * + * (c) CodeIgniter Foundation + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ #debug-icon { bottom: 0; From 9f9119e3584fa86c0795f9dc9a55e959609dca21 Mon Sep 17 00:00:00 2001 From: MGatner Date: Sat, 3 Jul 2021 23:35:28 +0000 Subject: [PATCH 052/490] Implement Deptrac --- .github/workflows/test-deptrac.yml | 82 ++++++++++ depfile.yaml | 232 +++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 .github/workflows/test-deptrac.yml create mode 100644 depfile.yaml diff --git a/.github/workflows/test-deptrac.yml b/.github/workflows/test-deptrac.yml new file mode 100644 index 000000000000..9a9179b501d8 --- /dev/null +++ b/.github/workflows/test-deptrac.yml @@ -0,0 +1,82 @@ +# When a PR is opened or a push is made, perform an +# architectural inspection on the code using Deptrac. +name: Deptrac + +on: + pull_request: + branches: + - 'develop' + - '4.*' + paths: + - 'app/**' + - 'system/**' + - 'composer.json' + - 'depfile.yaml' + - '.github/workflows/test-deptrac.yml' + push: + branches: + - 'develop' + - '4.*' + paths: + - 'app/**' + - 'system/**' + - 'composer.json' + - 'depfile.yaml' + - '.github/workflows/test-deptrac.yml' + +jobs: + build: + name: PHP ${{ matrix.php-versions }} Architectural Inspection + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + php-versions: ['7.4', '8.0'] + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + tools: composer, pecl, phive, phpunit + extensions: intl, json, mbstring, gd, mysqlnd, xdebug, xml, sqlite3 + + - name: Use latest Composer + run: composer self-update + + - name: Validate composer.json + run: composer validate --strict + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Create composer cache directory + run: mkdir -p ${{ steps.composer-cache.outputs.dir }} + + - name: Cache composer dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Create Deptrac cache directory + run: mkdir -p build/ + + - name: Cache Deptrac results + uses: actions/cache@v2 + with: + path: build + key: ${{ runner.os }}-deptrac-${{ github.sha }} + restore-keys: ${{ runner.os }}-deptrac- + + - name: Install dependencies + run: composer update --ansi --no-interaction + + - name: Run architectural inspection + run: | + sudo phive --no-progress install --global qossmic/deptrac --trust-gpg-keys B8F640134AB1782E + deptrac analyze --cache-file=build/deptrac.cache diff --git a/depfile.yaml b/depfile.yaml new file mode 100644 index 000000000000..147099379891 --- /dev/null +++ b/depfile.yaml @@ -0,0 +1,232 @@ +# Defines the layers for each framework +# component and their allowed interactions. +# The following components are exempt +# due to their global nature: +# - CLI & Commands +# - Config +# - Debug +# - Exception +# - Service +# - Validation\FormatRules +paths: + - ./app + - ./system +exclude_files: + - '#.*test.*#i' +layers: + - name: API + collectors: + - type: className + regex: ^Codeigniter\\API\\.* + - name: Cache + collectors: + - type: className + regex: ^Codeigniter\\Cache\\.* + - name: Controller + collectors: + - type: className + regex: ^CodeIgniter\\Controller$ + - name: Cookie + collectors: + - type: className + regex: ^Codeigniter\\Cookie\\.* + - name: Database + collectors: + - type: className + regex: ^Codeigniter\\Database\\.* + - name: Email + collectors: + - type: className + regex: ^Codeigniter\\Email\\.* + - name: Encryption + collectors: + - type: className + regex: ^Codeigniter\\Encryption\\.* + - name: Entity + collectors: + - type: className + regex: ^Codeigniter\\Entity\\.* + - name: Events + collectors: + - type: className + regex: ^Codeigniter\\Events\\.* + - name: Files + collectors: + - type: className + regex: ^Codeigniter\\Files\\.* + - name: Filters + collectors: + - type: bool + must: + - type: className + regex: ^Codeigniter\\Filters\\Filter.* + - name: Format + collectors: + - type: className + regex: ^Codeigniter\\Format\\.* + - name: Honeypot + collectors: + - type: className + regex: ^Codeigniter\\.*Honeypot.* # includes the Filter + - name: HTTP + collectors: + - type: bool + must: + - type: className + regex: ^Codeigniter\\HTTP\\.* + must_not: + - type: className + regex: (Exception|URI) + - name: I18n + collectors: + - type: className + regex: ^Codeigniter\\I18n\\.* + - name: Images + collectors: + - type: className + regex: ^Codeigniter\\Images\\.* + - name: Language + collectors: + - type: className + regex: ^Codeigniter\\Language\\.* + - name: Log + collectors: + - type: className + regex: ^Codeigniter\\Log\\.* + - name: Model + collectors: + - type: className + regex: ^Codeigniter\\.*Model$ + - name: Modules + collectors: + - type: className + regex: ^Codeigniter\\Modules\\.* + - name: Pager + collectors: + - type: className + regex: ^Codeigniter\\Pager\\.* + - name: Publisher + collectors: + - type: className + regex: ^Codeigniter\\Publisher\\.* + - name: RESTful + collectors: + - type: className + regex: ^Codeigniter\\RESTful\\.* + - name: Router + collectors: + - type: className + regex: ^Codeigniter\\Router\\.* + - name: Security + collectors: + - type: className + regex: ^Codeigniter\\Security\\.* + - name: Session + collectors: + - type: className + regex: ^Codeigniter\\Session\\.* + - name: Throttle + collectors: + - type: className + regex: ^Codeigniter\\Throttle\\.* + - name: Typography + collectors: + - type: className + regex: ^Codeigniter\\Typography\\.* + - name: URI + collectors: + - type: className + regex: ^CodeIgniter\\HTTP\\URI$ + - name: Validation + collectors: + - type: bool + must: + - type: className + regex: ^Codeigniter\\Validation\\.* + must_not: + - type: className + regex: ^Codeigniter\\Validation\\FormatRules$ + - name: View + collectors: + - type: className + regex: ^Codeigniter\\View\\.* +ruleset: + API: + - Format + - HTTP + Controller: + - HTTP + - Validation + Database: + - Entity + - Events + Email: + - Events + Entity: + - I18n + Filters: + - HTTP + Honeypot: + - Filters + - HTTP + HTTP: + - Cookie + - Files + - URI + Images: + - Files + Model: + - Database + - I18n + - Pager + - Validation + Pager: + - URI + - View + Publisher: + - Files + - URI + RESTful: # @todo Transitive Dependency + - API + - Controller + - Format + - HTTP + - Validation + Router: + - HTTP + Security: + - Cookie + - HTTP + Session: + - Cookie + - Database + Throttle: + - Cache + Validation: + - HTTP + View: + - Cache +skip_violations: + # Individual class exemptions + CodeIgniter\Entity\Cast\URICast: + - CodeIgniter\HTTP\URI + CodeIgniter\Log\Handlers\ChromeLoggerHandler: + - CodeIgniter\HTTP\ResponseInterface + CodeIgniter\View\Table: + - CodeIgniter\Database\BaseResult + CodeIgniter\View\Plugins: + - CodeIgniter\HTTP\URI + + # BC changes that should be fixed + CodeIgniter\HTTP\ResponseTrait: + - CodeIgniter\Pager\PagerInterface + CodeIgniter\HTTP\ResponseInterface: + - CodeIgniter\Pager\PagerInterface + CodeIgniter\HTTP\Response: + - CodeIgniter\Pager\PagerInterface + CodeIgniter\HTTP\RedirectResponse: + - CodeIgniter\Pager\PagerInterface + CodeIgniter\HTTP\DownloadResponse: + - CodeIgniter\Pager\PagerInterface + CodeIgniter\Validation\Validation: + - CodeIgniter\View\RendererInterface From d27708ce186e41f07314fdf890cda8b3d42aa2ad Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 15 Jul 2021 15:34:20 +0000 Subject: [PATCH 053/490] Simplify Action workflow --- .github/workflows/test-deptrac.yml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-deptrac.yml b/.github/workflows/test-deptrac.yml index 9a9179b501d8..0698809319fe 100644 --- a/.github/workflows/test-deptrac.yml +++ b/.github/workflows/test-deptrac.yml @@ -26,12 +26,8 @@ on: jobs: build: - name: PHP ${{ matrix.php-versions }} Architectural Inspection + name: Architectural Inspection runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - php-versions: ['7.4', '8.0'] steps: - name: Checkout uses: actions/checkout@v2 @@ -39,13 +35,10 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php-versions }} - tools: composer, pecl, phive, phpunit + php-version: '8.0' + tools: composer, phive extensions: intl, json, mbstring, gd, mysqlnd, xdebug, xml, sqlite3 - - name: Use latest Composer - run: composer self-update - - name: Validate composer.json run: composer validate --strict From 91ca8f36081abbc81b8d5954dc17841454327732 Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 15 Jul 2021 15:43:44 +0000 Subject: [PATCH 054/490] Implement transitive dependencies --- depfile.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/depfile.yaml b/depfile.yaml index 147099379891..c1d8b87c3db2 100644 --- a/depfile.yaml +++ b/depfile.yaml @@ -186,12 +186,9 @@ ruleset: Publisher: - Files - URI - RESTful: # @todo Transitive Dependency - - API - - Controller - - Format - - HTTP - - Validation + RESTful: + - +API + - +Controller Router: - HTTP Security: From 6e64994e6cd4569406ff6c052735cfddfda75efd Mon Sep 17 00:00:00 2001 From: "monken,wu" <610877102@mail.nknu.edu.tw> Date: Tue, 20 Jul 2021 02:13:39 +0800 Subject: [PATCH 055/490] fix entity-datamap guide wrong (#4937) --- system/Entity/Entity.php | 2 +- user_guide_src/source/models/entities.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 7d44adf3e14c..a98395172e23 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -41,7 +41,7 @@ class Entity implements JsonSerializable * * Example: * $datamap = [ - * 'db_name' => 'class_name' + * 'class_name' => 'db_name' * ]; */ protected $datamap = []; diff --git a/user_guide_src/source/models/entities.rst b/user_guide_src/source/models/entities.rst index 247c0fdce0e3..eec2447adf06 100644 --- a/user_guide_src/source/models/entities.rst +++ b/user_guide_src/source/models/entities.rst @@ -275,13 +275,13 @@ simply map the ``full_name`` column in the database to the ``$name`` property, a ]; protected $datamap = [ - 'full_name' => 'name', + 'name' => 'full_name', ]; } By adding our new database name to the ``$datamap`` array, we can tell the class what class property the database column -should be accessible through. The key of the array is the name of the column in the database, where the value in the array -is class property to map it to. +should be accessible through. The key of the array is class property to map it to, where the value in the array is the +name of the column in the database. In this example, when the model sets the ``full_name`` field on the User class, it actually assigns that value to the class' ``$name`` property, so it can be set and retrieved through ``$user->name``. The value will still be accessible From eeaae2a2186189cfe8ed99653c5c010487dcdf02 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Fri, 23 Jul 2021 21:16:56 +0800 Subject: [PATCH 056/490] Fix splitting of string rules (#4957) --- system/Validation/Validation.php | 43 +++++++++++---- tests/system/Validation/ValidationTest.php | 64 ++++++++++++++++++++++ 2 files changed, 96 insertions(+), 11 deletions(-) diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index a41e1ebc558f..027b0018e842 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -672,20 +672,41 @@ protected function getErrorMessage(string $rule, string $field, ?string $label = */ protected function splitRules(string $rules): array { - $nonEscapeBracket = '((?getPrivateMethodInvoker($this->validation, 'splitRules'); + $this->assertSame($expected, $splitter($input)); + } + + public function provideStringRulesCases(): iterable + { + yield [ + 'required', + ['required'], + ]; + + yield [ + 'required|numeric', + ['required', 'numeric'], + ]; + + yield [ + 'required|max_length[500]|hex', + ['required', 'max_length[500]', 'hex'], + ]; + + yield [ + 'required|numeric|regex_match[/[a-zA-Z]+/]', + ['required', 'numeric', 'regex_match[/[a-zA-Z]+/]'], + ]; + + yield [ + 'required|max_length[500]|regex_match[/^;"\'{}\[\]^<>=/]', + ['required', 'max_length[500]', 'regex_match[/^;"\'{}\[\]^<>=/]'], + ]; + + yield [ + 'regex_match[/^;"\'{}\[\]^<>=/]|regex_match[/[^a-z0-9.\|_]+/]', + ['regex_match[/^;"\'{}\[\]^<>=/]', 'regex_match[/[^a-z0-9.\|_]+/]'], + ]; + + yield [ + 'required|regex_match[/^(01[2689]|09)[0-9]{8}$/]|numeric', + ['required', 'regex_match[/^(01[2689]|09)[0-9]{8}$/]', 'numeric'], + ]; + + yield [ + 'required|regex_match[/^[0-9]{4}[\-\.\[\/][0-9]{2}[\-\.\[\/][0-9]{2}/]|max_length[10]', + ['required', 'regex_match[/^[0-9]{4}[\-\.\[\/][0-9]{2}[\-\.\[\/][0-9]{2}/]', 'max_length[10]'], + ]; + + yield [ + 'required|regex_match[/^(01|2689|09)[0-9]{8}$/]|numeric', + ['required', 'regex_match[/^(01|2689|09)[0-9]{8}$/]', 'numeric'], + ]; + } } From cc24a929ea7cec4dce0b106bb3c98c458f508e05 Mon Sep 17 00:00:00 2001 From: Wolf Date: Mon, 2 Aug 2021 14:16:47 +0200 Subject: [PATCH 057/490] CLI: Prompt: Introduce promptByKey method --- system/CLI/CLI.php | 49 ++++++++++++++++++++--- user_guide_src/source/cli/cli_library.rst | 34 +++++++++++++++- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index b463aa6a3016..3ddd544c229c 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -241,25 +241,64 @@ public static function prompt(string $field, $options = null, $validation = null if (empty($opts)) { $extraOutput = $extraOutputDefault; } else { - $extraOutput = ' [' . $extraOutputDefault . ', ' . implode(', ', $opts) . ']'; - $validation[] = 'in_list[' . implode(',', $options) . ']'; + $extraOutput = '[' . $extraOutputDefault . ', ' . implode(', ', $opts) . ']'; + $validation[] = 'in_list[' . implode(', ', $options) . ']'; } $default = $options[0]; } - static::fwrite(STDOUT, $field . $extraOutput . ': '); + static::fwrite(STDOUT, $field . (trim($field) ? ' ' : '') . $extraOutput . ': '); // Read the input from keyboard. $input = trim(static::input()) ?: $default; if ($validation) { - while (! static::validate($field, $input, $validation)) { + while (! static::validate(trim($field), $input, $validation)) { $input = static::prompt($field, $options, $validation); } } - return empty($input) ? '' : $input; + return $input; + } + + /** + * prompt(), but based on the option's key + * + * @param array|string $text Output "field" text or an one or two value array where the first value is the text before listing the options + * and the second value the text before asking to select one option. Provide empty string to omit + * @param array $options A list of options (array(key => description)), the first option will be the default value + * @param array|string|null $validation Validation rules + * + * @return string The selected key of $options + * + * @codeCoverageIgnore + */ + public static function promptByKey($text, array $options, $validation = null): string + { + if (is_string($text)) { + $text = [$text]; + } elseif (! is_array($text)) { + throw new InvalidArgumentException('$text can only be of type string|array'); + } + + if (! $options) { + throw new InvalidArgumentException('No options to select from were provided'); + } + + if ($line = array_shift($text)) { + CLI::write($line); + } + + // +2 for the square brackets around the key + $keyMaxLength = max(array_map('mb_strwidth', array_keys($options))) + 2; + + foreach ($options as $key => $description) { + $name = str_pad(' [' . $key . '] ', $keyMaxLength + 4, ' '); + CLI::write(CLI::color($name, 'green') . CLI::wrap($description, 125, $keyMaxLength + 4)); + } + + return static::prompt(PHP_EOL . array_shift($text), array_keys($options), $validation); } /** diff --git a/user_guide_src/source/cli/cli_library.rst b/user_guide_src/source/cli/cli_library.rst index d7ddc0993015..41638272eaa5 100644 --- a/user_guide_src/source/cli/cli_library.rst +++ b/user_guide_src/source/cli/cli_library.rst @@ -38,7 +38,7 @@ Getting Input from the User Sometimes you need to ask the user for more information. They might not have provided optional command-line arguments, or the script may have encountered an existing file and needs confirmation before overwriting. This is -handled with the ``prompt()`` method. +handled with the ``prompt()`` or ``promptByKey()`` method. You can provide a question by passing it in as the first parameter:: @@ -61,6 +61,38 @@ Validation rules can also be written in the array syntax.:: $email = CLI::prompt('What is your email?', null, ['required', 'valid_email']); + +**promptByKey()** + +Predefined answers (options) for prompt sometimes need to be described or are too complex to select via their value. +``promptByKey()`` allows the user to select an option by its key instead of its value:: + + $fruit = CLI::promptByKey('These are your choices:', ['The red apple', 'The plump orange', 'The ripe banana']); + + //These are your choices: + // [0] The red apple + // [1] The plump orange + // [2] The ripe banana + // + //[0, 1, 2]: + +Named keys are also possible:: + + $fruit = CLI::promptByKey(['These are your choices:', 'Which would you like?'], [ + 'apple' => 'The red apple', + 'orange' => 'The plump orange', + 'banana' => 'The ripe banana' + ]); + + //These are your choices: + // [apple] The red apple + // [orange] The plump orange + // [banana] The ripe banana + // + //Which would you like? [apple, orange, banana]: + +Finally, you can pass :ref:`validation ` rules to the answer input as the third parameter, the acceptable answers are automatically restricted to the passed options. + Providing Feedback ================== From c5f82f77157345c9d15057997798111af9b7e4b3 Mon Sep 17 00:00:00 2001 From: Wolf Date: Mon, 2 Aug 2021 16:31:59 +0200 Subject: [PATCH 058/490] CLI: Prompt: Change color of default value to green --- system/CLI/CLI.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 3ddd544c229c..ea1e1ad85611 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -228,13 +228,13 @@ public static function prompt(string $field, $options = null, $validation = null } if (is_string($options)) { - $extraOutput = ' [' . static::color($options, 'white') . ']'; + $extraOutput = ' [' . static::color($options, 'green') . ']'; $default = $options; } if (is_array($options) && $options) { $opts = $options; - $extraOutputDefault = static::color($opts[0], 'white'); + $extraOutputDefault = static::color($opts[0], 'green'); unset($opts[0]); From 5561492918feb6f8e3136a33f61990bd10385675 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Fri, 6 Aug 2021 21:07:03 +0800 Subject: [PATCH 059/490] Fix adding foreign keys with only string fields (#4988) --- system/Database/Forge.php | 19 ++++++++-- .../20160428212500_Create_test_tables.php | 2 - tests/system/Database/Live/ForgeTest.php | 37 +++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/system/Database/Forge.php b/system/Database/Forge.php index ba3423a890ea..256010e68d2d 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -351,12 +351,25 @@ public function addField($field) throw new InvalidArgumentException('Field information is required for that operation.'); } - $this->fields[] = $field; + $fieldName = explode(' ', $field, 2)[0]; + $fieldName = trim($fieldName, '`\'"'); + + $this->fields[$fieldName] = $field; } } if (is_array($field)) { - $this->fields = array_merge($this->fields, $field); + foreach ($field as $idx => $f) { + if (is_string($f)) { + $this->addField($f); + + continue; + } + + if (is_array($f)) { + $this->fields = array_merge($this->fields, [$idx => $f]); + } + } } return $this; @@ -761,7 +774,7 @@ protected function _processFields(bool $createTable = false): array $fields = []; foreach ($this->fields as $key => $attributes) { - if (is_int($key) && ! is_array($attributes)) { + if (! is_array($attributes)) { $fields[] = ['_literal' => $attributes]; continue; diff --git a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php index 5b414de228a7..294accbdb898 100644 --- a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php +++ b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php @@ -137,7 +137,6 @@ public function up() 'data' => ['type' => 'BLOB', 'null' => false], ]); $this->forge->addKey('id', true); - $this->forge->addKey('timestamp'); $this->forge->createTable('ci_sessions', true); } @@ -149,7 +148,6 @@ public function up() "data bytea DEFAULT '' NOT NULL", ]); $this->forge->addKey('id', true); - $this->forge->addKey('timestamp'); $this->forge->createTable('ci_sessions', true); } } diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 69812f98444b..2eaaa9f2af7b 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -427,6 +427,43 @@ public function testForeignKey() $this->forge->dropTable('forge_test_users', true); } + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/4986 + */ + public function testForeignKeyAddingWithStringFields() + { + if ($this->db->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Testing only on MySQLi but fix expands to all DBs.'); + } + + $attributes = ['ENGINE' => 'InnoDB']; + + $this->forge->addField([ + '`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY', + '`name` VARCHAR(255) NOT NULL', + ])->createTable('forge_test_users', true, $attributes); + + $this->forge + ->addField([ + '`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY', + '`users_id` INT(11) NOT NULL', + '`name` VARCHAR(255) NOT NULL', + ]) + ->addForeignKey('users_id', 'forge_test_users', 'id', 'CASCADE', 'CASCADE') + ->createTable('forge_test_invoices', true, $attributes); + + $foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices')[0]; + + $this->assertSame($this->db->DBPrefix . 'forge_test_invoices_users_id_foreign', $foreignKeyData->constraint_name); + $this->assertSame('users_id', $foreignKeyData->column_name); + $this->assertSame('id', $foreignKeyData->foreign_column_name); + $this->assertSame($this->db->DBPrefix . 'forge_test_invoices', $foreignKeyData->table_name); + $this->assertSame($this->db->DBPrefix . 'forge_test_users', $foreignKeyData->foreign_table_name); + + $this->forge->dropTable('forge_test_invoices', true); + $this->forge->dropTable('forge_test_users', true); + } + public function testForeignKeyFieldNotExistException() { $this->expectException(DatabaseException::class); From 68a9a353a34b6e34b190d6c3db547990b30b3973 Mon Sep 17 00:00:00 2001 From: Paulo Esteves Date: Sun, 15 Aug 2021 00:27:10 +0100 Subject: [PATCH 060/490] Fix lang() function is overriding locale --- system/Common.php | 21 +++++++++++++++++++-- tests/system/Language/LanguageTest.php | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/system/Common.php b/system/Common.php index bacac6ac0e4b..bdea46b26564 100644 --- a/system/Common.php +++ b/system/Common.php @@ -717,8 +717,25 @@ function is_really_writable(string $file): bool */ function lang(string $line, array $args = [], ?string $locale = null) { - return Services::language($locale) - ->getLine($line, $args); + $language = Services::language(); + + //Get active locale + $activeLocale = $language->getLocale(); + + if ($locale && $locale != $activeLocale) + { + $language->setLocale($locale); + } + + $line = $language->getLine($line, $args); + + if ($locale && $locale != $activeLocale) + { + //Reset to active locale + $language->setLocale($activeLocale); + } + + return $line; } } diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php index 0dd8b3d69adc..ace99172aac4 100644 --- a/tests/system/Language/LanguageTest.php +++ b/tests/system/Language/LanguageTest.php @@ -271,6 +271,25 @@ public function testBaseFallbacks() $this->assertSame('More.shootMe', $this->lang->getLine('More.shootMe')); } + /** + * Test if after using lang() with a locale the Language class keep the locale after return the $line + */ + public function testLangKeepLocale() + { + $this->lang = Services::language('en', true); + + lang('Language.languageGetLineInvalidArgumentException'); + $this->assertEquals('en', $this->lang->getLocale()); + + lang('Language.languageGetLineInvalidArgumentException', [], 'ru'); + $this->assertEquals('en', $this->lang->getLocale()); + + lang('Language.languageGetLineInvalidArgumentException'); + $this->assertEquals('en', $this->lang->getLocale()); + } + + //-------------------------------------------------------------------- + /** * Testing base locale vs variants, with fallback to English. * From ef2c3b4400f058a00411bee0cac7d417dd7bfaa0 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 4 Jul 2021 10:20:59 +0200 Subject: [PATCH 061/490] Fix bug with handling boolean values in set() method in BaseBuilder and Model class --- system/Database/BaseBuilder.php | 4 +- system/Database/SQLSRV/Forge.php | 4 + system/Database/SQLite3/Forge.php | 4 + system/Model.php | 8 +- .../20160428212500_Create_test_tables.php | 1 + .../_support/Database/Seeds/CITestSeeder.php | 4 +- tests/system/Database/Builder/UpdateTest.php | 74 +++++++++++++++++++ tests/system/Database/Live/UpdateTest.php | 19 +++++ .../source/installation/upgrade_420.rst | 12 +++ 9 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 user_guide_src/source/installation/upgrade_420.rst diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 7c3253c52685..43cedc8f09eb 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -1339,12 +1339,12 @@ protected function _limit(string $sql, bool $offsetIgnore = false): string * Allows key/value pairs to be set for insert(), update() or replace(). * * @param array|object|string $key Field name, or an array of field/value pairs - * @param string|null $value Field value, if $key is a single field + * @param mixed $value Field value, if $key is a single field * @param bool|null $escape Whether to escape values and identifiers * * @return $this */ - public function set($key, ?string $value = '', ?bool $escape = null) + public function set($key, $value = '', ?bool $escape = null) { $key = $this->objectToArray($key); diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index aa646ccf2d6f..28dfe38c9a05 100755 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -349,6 +349,10 @@ protected function _attributeType(array &$attributes) $attributes['TYPE'] = 'DATETIME'; break; + case 'BOOLEAN': + $attributes['TYPE'] = 'BIT'; + break; + default: break; } diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index 4d118d04eb62..3275322beb92 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -198,6 +198,10 @@ protected function _attributeType(array &$attributes) $attributes['TYPE'] = 'TEXT'; break; + case 'BOOLEAN': + $attributes['TYPE'] = 'INT'; + break; + default: break; } diff --git a/system/Model.php b/system/Model.php index 3617f83f69cb..580c691247f6 100644 --- a/system/Model.php +++ b/system/Model.php @@ -557,13 +557,13 @@ public function builder(?string $table = null) * data here. This allows it to be used with any of the other * builder methods and still get validated data, like replace. * - * @param array|string $key Field name, or an array of field/value pairs - * @param string|null $value Field value, if $key is a single field - * @param bool|null $escape Whether to escape values and identifiers + * @param mixed $key Field name, or an array of field/value pairs + * @param mixed $value Field value, if $key is a single field + * @param bool|null $escape Whether to escape values and identifiers * * @return $this */ - public function set($key, ?string $value = '', ?bool $escape = null) + public function set($key, $value = '', ?bool $escape = null) { $data = is_array($key) ? $key : [$key => $value]; diff --git a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php index 294accbdb898..cf517e988ebc 100644 --- a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php +++ b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php @@ -70,6 +70,7 @@ public function up() 'type_double' => ['type' => 'DOUBLE', 'null' => true], 'type_decimal' => ['type' => 'DECIMAL', 'constraint' => '18,4', 'null' => true], 'type_blob' => ['type' => 'BLOB', 'null' => true], + 'type_boolean' => ['type' => 'BOOLEAN', 'null' => true], ]; if ($this->db->DBDriver === 'Postgre') { diff --git a/tests/_support/Database/Seeds/CITestSeeder.php b/tests/_support/Database/Seeds/CITestSeeder.php index ae4b2af3b0b7..372697aaa6ba 100644 --- a/tests/_support/Database/Seeds/CITestSeeder.php +++ b/tests/_support/Database/Seeds/CITestSeeder.php @@ -106,6 +106,7 @@ public function run() 'type_datetime' => '2020-06-18T05:12:24.000+02:00', 'type_timestamp' => '2019-07-18T21:53:21.000+02:00', 'type_bigint' => 2342342, + 'type_boolean' => 1, ], ], ]; @@ -119,7 +120,8 @@ public function run() } if ($this->db->DBDriver === 'Postgre') { - $data['type_test'][0]['type_time'] = '15:22:00'; + $data['type_test'][0]['type_time'] = '15:22:00'; + $data['type_test'][0]['type_boolean'] = true; unset( $data['type_test'][0]['type_enum'], $data['type_test'][0]['type_set'], diff --git a/tests/system/Database/Builder/UpdateTest.php b/tests/system/Database/Builder/UpdateTest.php index 801324d01f79..6d4cc84973c1 100644 --- a/tests/system/Database/Builder/UpdateTest.php +++ b/tests/system/Database/Builder/UpdateTest.php @@ -99,6 +99,80 @@ public function testUpdateWithSet() $this->assertSame($expectedBinds, $builder->getBinds()); } + public function testUpdateWithSetAsInt() + { + $builder = new BaseBuilder('jobs', $this->db); + + $builder->testMode()->set('age', 22)->where('id', 1)->update(null, null, null); + + $expectedSQL = 'UPDATE "jobs" SET "age" = 22 WHERE "id" = 1'; + $expectedBinds = [ + 'age' => [ + 22, + true, + ], + 'id' => [ + 1, + true, + ], + ]; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate())); + $this->assertSame($expectedBinds, $builder->getBinds()); + } + + public function testUpdateWithSetAsBoolean() + { + $builder = new BaseBuilder('jobs', $this->db); + + $builder->testMode()->set('manager', true)->where('id', 1)->update(null, null, null); + + $expectedSQL = 'UPDATE "jobs" SET "manager" = 1 WHERE "id" = 1'; + $expectedBinds = [ + 'manager' => [ + true, + true, + ], + 'id' => [ + 1, + true, + ], + ]; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate())); + $this->assertSame($expectedBinds, $builder->getBinds()); + } + + public function testUpdateWithSetAsArray() + { + $builder = new BaseBuilder('jobs', $this->db); + + $builder->testMode()->set(['name' => 'Programmer', 'age' => 22, 'manager' => true])->where('id', 1)->update(null, null, null); + + $expectedSQL = 'UPDATE "jobs" SET "name" = \'Programmer\', "age" = 22, "manager" = 1 WHERE "id" = 1'; + $expectedBinds = [ + 'name' => [ + 'Programmer', + true, + ], + 'age' => [ + 22, + true, + ], + 'manager' => [ + true, + true, + ], + 'id' => [ + 1, + true, + ], + ]; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate())); + $this->assertSame($expectedBinds, $builder->getBinds()); + } + public function testUpdateThrowsExceptionWithNoData() { $builder = new BaseBuilder('jobs', $this->db); diff --git a/tests/system/Database/Live/UpdateTest.php b/tests/system/Database/Live/UpdateTest.php index f047f16a7bfd..c0f1bd3575d4 100644 --- a/tests/system/Database/Live/UpdateTest.php +++ b/tests/system/Database/Live/UpdateTest.php @@ -211,4 +211,23 @@ public function testSetWithoutEscape() 'description' => 'Developer', ]); } + + public function testSetWithBoolean() + { + $this->db->table('type_test') + ->set('type_boolean', false) + ->update(); + + $this->seeInDatabase('type_test', [ + 'type_boolean' => false, + ]); + + $this->db->table('type_test') + ->set('type_boolean', true) + ->update(); + + $this->seeInDatabase('type_test', [ + 'type_boolean' => true, + ]); + } } diff --git a/user_guide_src/source/installation/upgrade_420.rst b/user_guide_src/source/installation/upgrade_420.rst new file mode 100644 index 000000000000..a306b4978347 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_420.rst @@ -0,0 +1,12 @@ +############################# +Upgrading from 4.1.3 to 4.2.0 +############################# + +**Changes for set() method in BaseBuilder and Model class** + +The casting for the ``$value`` parameter has been removed to fix a bug where passing parameters as array and string +to the ``set()`` method were handled differently. If you extended the ``BaseBuilder`` class or ``Model`` class yourself +and modified the ``set()`` method, then you need to change its definition from +``public function set($key, ?string $value = '', ?bool $escape = null)`` to +``public function set($key, $value = '', ?bool $escape = null)``. + From c6a913731c982547a5f2008f7d4628bdbab1ae98 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 4 Jul 2021 10:31:25 +0200 Subject: [PATCH 062/490] Update method definition in the user guide --- user_guide_src/source/database/query_builder.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 342da67461e3..3494325e5084 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -1726,7 +1726,7 @@ Class Reference .. php:method:: set($key[, $value = ''[, $escape = null]]) :param mixed $key: Field name, or an array of field/value pairs - :param string $value: Field value, if $key is a single field + :param mixed $value: Field value, if $key is a single field :param bool $escape: Whether to escape values and identifiers :returns: ``BaseBuilder`` instance (method chaining) :rtype: ``BaseBuilder`` From e8ddf0e6d5d2de027afa05317bd9e5950522e125 Mon Sep 17 00:00:00 2001 From: michalsn Date: Fri, 23 Jul 2021 09:53:19 +0200 Subject: [PATCH 063/490] add to toctree --- user_guide_src/source/changelogs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index 038d259e4189..a997a57f0c3f 100644 --- a/user_guide_src/source/changelogs/index.rst +++ b/user_guide_src/source/changelogs/index.rst @@ -12,6 +12,7 @@ See all the changes. .. toctree:: :titlesonly: + v4.2.0 v4.1.4 v4.1.3 v4.1.2 From 1c51f25e8a4c2684fcd4b6b29c27a122af56d7ba Mon Sep 17 00:00:00 2001 From: MGatner Date: Tue, 17 Aug 2021 14:53:13 +0000 Subject: [PATCH 064/490] Add config for cache keys --- app/Config/Cache.php | 15 +++++++++++++++ system/Cache/Handlers/BaseHandler.php | 10 +++++++--- .../system/Cache/Handlers/BaseHandlerTest.php | 10 ++++++++++ user_guide_src/source/changelogs/v4.2.0.rst | 18 ++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 user_guide_src/source/changelogs/v4.2.0.rst diff --git a/app/Config/Cache.php b/app/Config/Cache.php index ce1db4d7f24e..3c725c469754 100644 --- a/app/Config/Cache.php +++ b/app/Config/Cache.php @@ -2,6 +2,7 @@ namespace Config; +use CodeIgniter\Cache\Handlers\BaseHandler; use CodeIgniter\Cache\Handlers\DummyHandler; use CodeIgniter\Cache\Handlers\FileHandler; use CodeIgniter\Cache\Handlers\MemcachedHandler; @@ -97,6 +98,20 @@ class Cache extends BaseConfig */ public $ttl = 60; + /** + * -------------------------------------------------------------------------- + * Reserved Characters + * -------------------------------------------------------------------------- + * + * A string of reserved characters that will not be allowed in keys or tags. + * Strings that violate this restriction will cause handlers to throw. + * Default: {}()/\@: + * Note: The default set is required for PSR-6 compliance. + * + * @var string + */ + public $reservedCharacters = '{}()/\@:'; + /** * -------------------------------------------------------------------------- * File settings diff --git a/system/Cache/Handlers/BaseHandler.php b/system/Cache/Handlers/BaseHandler.php index cae03b901664..57120208ceeb 100644 --- a/system/Cache/Handlers/BaseHandler.php +++ b/system/Cache/Handlers/BaseHandler.php @@ -22,8 +22,10 @@ abstract class BaseHandler implements CacheInterface { /** - * Reserved characters that cannot be used in a key or tag. + * Reserved characters that cannot be used in a key or tag. May be overridden by the config. * From https://github.com/symfony/cache-contracts/blob/c0446463729b89dd4fa62e9aeecc80287323615d/ItemInterface.php#L43 + * + * @deprecated in favor of the Cache config */ public const RESERVED_CHARACTERS = '{}()/\@:'; @@ -58,8 +60,10 @@ public static function validateKey($key, $prefix = ''): string if ($key === '') { throw new InvalidArgumentException('Cache key cannot be empty.'); } - if (strpbrk($key, self::RESERVED_CHARACTERS) !== false) { - throw new InvalidArgumentException('Cache key contains reserved characters ' . self::RESERVED_CHARACTERS); + + $reserved = config('Cache')->reservedCharacters ?? self::RESERVED_CHARACTERS; + if ($reserved && strpbrk($key, $reserved) !== false) { + throw new InvalidArgumentException('Cache key contains reserved characters ' . $reserved); } // If the key with prefix exceeds the length then return the hashed version diff --git a/tests/system/Cache/Handlers/BaseHandlerTest.php b/tests/system/Cache/Handlers/BaseHandlerTest.php index 5965666a5810..73f46620b559 100644 --- a/tests/system/Cache/Handlers/BaseHandlerTest.php +++ b/tests/system/Cache/Handlers/BaseHandlerTest.php @@ -44,6 +44,16 @@ public function invalidTypeProvider(): array ]; } + public function testValidateKeyUsesConfig() + { + config('Cache')->reservedCharacters = 'b'; + + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Cache key contains reserved characters b'); + + BaseHandler::validateKey('banana'); + } + public function testValidateKeySuccess() { $string = 'banana'; diff --git a/user_guide_src/source/changelogs/v4.2.0.rst b/user_guide_src/source/changelogs/v4.2.0.rst new file mode 100644 index 000000000000..c1a7d2673ab1 --- /dev/null +++ b/user_guide_src/source/changelogs/v4.2.0.rst @@ -0,0 +1,18 @@ +Version 4.2.0 +============= + +Release Date: Not released + +**4.2.0 release of CodeIgniter4** + +Enhancements: + +- Added Cache config for reserved characters + +Changes: + +Deprecations: + +- Deprecated ``CodeIgniter\\Cache\\Handlers\\BaseHandler::RESERVED_CHARACTERS`` in favor of the new config property + +Bugs Fixed: From 18f24101d1ae8704d76f535fb921f9dd31cb9384 Mon Sep 17 00:00:00 2001 From: MGatner Date: Wed, 18 Aug 2021 07:49:41 -0400 Subject: [PATCH 065/490] Update app/Config/Cache.php --- app/Config/Cache.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Config/Cache.php b/app/Config/Cache.php index 3c725c469754..2d1fea90c9d1 100644 --- a/app/Config/Cache.php +++ b/app/Config/Cache.php @@ -2,7 +2,6 @@ namespace Config; -use CodeIgniter\Cache\Handlers\BaseHandler; use CodeIgniter\Cache\Handlers\DummyHandler; use CodeIgniter\Cache\Handlers\FileHandler; use CodeIgniter\Cache\Handlers\MemcachedHandler; From 39ad669599517f6db23d73cb87f756252f6bb3bf Mon Sep 17 00:00:00 2001 From: MonkenWu <610877102@mail.nknu.edu.tw> Date: Wed, 14 Jul 2021 01:34:15 +0800 Subject: [PATCH 066/490] =?UTF-8?q?=EF=BB=BFFix=20bug=20with=20DB-Forge=20?= =?UTF-8?q?composite=20foreign=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- system/Database/Forge.php | 54 +++++++--- system/Database/SQLSRV/Forge.php | 29 ++--- tests/system/Database/Live/ForgeTest.php | 129 +++++++++++++++++++++++ user_guide_src/source/dbmgmt/forge.rst | 14 ++- 4 files changed, 193 insertions(+), 33 deletions(-) diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 256010e68d2d..39d4384a98ae 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -378,21 +378,35 @@ public function addField($field) /** * Add Foreign Key * + * @param string|string[] $fieldName + * @param string|string[] $tableField + * * @throws DatabaseException * * @return Forge */ - public function addForeignKey(string $fieldName = '', string $tableName = '', string $tableField = '', string $onUpdate = '', string $onDelete = '') + public function addForeignKey($fieldName = '', string $tableName = '', $tableField = '', string $onUpdate = '', string $onDelete = '') { - if (! isset($this->fields[$fieldName])) { - throw new DatabaseException(lang('Database.fieldNotExists', [$fieldName])); + $fieldName = (array) $fieldName; + $tableField = (array) $tableField; + $errorNames = []; + + foreach ($fieldName as $name) { + if (! isset($this->fields[$name])) { + $errorNames[] = $name; + } + } + + if ($errorNames !== []) { + throw new DatabaseException(lang('Database.fieldNotExists', $errorNames)); } - $this->foreignKeys[$fieldName] = [ - 'table' => $tableName, - 'field' => $tableField, - 'onDelete' => strtoupper($onDelete), - 'onUpdate' => strtoupper($onUpdate), + $this->foreignKeys[] = [ + 'field' => $fieldName, + 'referenceTable' => $tableName, + 'referenceField' => $tableField, + 'onDelete' => strtoupper($onDelete), + 'onUpdate' => strtoupper($onUpdate), ]; return $this; @@ -1009,18 +1023,24 @@ protected function _processForeignKeys(string $table): string 'SET DEFAULT', ]; - foreach ($this->foreignKeys as $field => $fkey) { - $nameIndex = $table . '_' . $field . '_foreign'; + if ($this->foreignKeys !== []) { + foreach ($this->foreignKeys as $fkey) { + $nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign'; + $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex); + $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field'])); + $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']); + $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField'])); - $sql .= ",\n\tCONSTRAINT " . $this->db->escapeIdentifiers($nameIndex) - . ' FOREIGN KEY(' . $this->db->escapeIdentifiers($field) . ') REFERENCES ' . $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['table']) . ' (' . $this->db->escapeIdentifiers($fkey['field']) . ')'; + $formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)"; + $sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled); - if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { - $sql .= ' ON DELETE ' . $fkey['onDelete']; - } + if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { + $sql .= ' ON DELETE ' . $fkey['onDelete']; + } - if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) { - $sql .= ' ON UPDATE ' . $fkey['onUpdate']; + if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) { + $sql .= ' ON UPDATE ' . $fkey['onUpdate']; + } } } diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 28dfe38c9a05..3fed6c48962f 100755 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -282,19 +282,24 @@ protected function _processForeignKeys(string $table): string $allowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT']; - foreach ($this->foreignKeys as $field => $fkey) { - $nameIndex = $table . '_' . $field . '_foreign'; - - $sql .= ",\n\t CONSTRAINT " . $this->db->escapeIdentifiers($nameIndex) - . ' FOREIGN KEY (' . $this->db->escapeIdentifiers($field) . ') ' - . ' REFERENCES ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($this->db->getPrefix() . $fkey['table']) . ' (' . $this->db->escapeIdentifiers($fkey['field']) . ')'; - - if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { - $sql .= ' ON DELETE ' . $fkey['onDelete']; - } + if ($this->foreignKeys !== []) { + foreach ($this->foreignKeys as $fkey) { + $nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign'; + $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex); + $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field'])); + $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']); + $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField'])); + + $formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)"; + $sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled); + + if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { + $sql .= ' ON DELETE ' . $fkey['onDelete']; + } - if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) { - $sql .= ' ON UPDATE ' . $fkey['onUpdate']; + if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) { + $sql .= ' ON UPDATE ' . $fkey['onUpdate']; + } } } diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 2eaaa9f2af7b..dadcd753e3b5 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -464,6 +464,135 @@ public function testForeignKeyAddingWithStringFields() $this->forge->dropTable('forge_test_users', true); } + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/4310 + */ + public function testCompositeForeignKey() + { + $attributes = []; + + if ($this->db->DBDriver === 'MySQLi') { + $attributes = ['ENGINE' => 'InnoDB']; + } + + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'second_id' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + ]); + $this->forge->addPrimaryKey(['id', 'second_id']); + $this->forge->createTable('forge_test_users', true, $attributes); + + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'users_id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'users_second_id' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + ]); + $this->forge->addPrimaryKey('id'); + $this->forge->addForeignKey(['users_id', 'users_second_id'], 'forge_test_users', ['id', 'second_id'], 'CASCADE', 'CASCADE'); + + $this->forge->createTable('forge_test_invoices', true, $attributes); + + $foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices'); + + if ($this->db->DBDriver === 'SQLite3') { + $this->assertSame($foreignKeyData[0]->constraint_name, 'users_id to db_forge_test_users.id'); + $this->assertSame($foreignKeyData[0]->sequence, 0); + $this->assertSame($foreignKeyData[1]->constraint_name, 'users_second_id to db_forge_test_users.second_id'); + $this->assertSame($foreignKeyData[1]->sequence, 1); + } else { + $haystack = ['users_id', 'users_second_id']; + $this->assertSame($foreignKeyData[0]->constraint_name, $this->db->DBPrefix . 'forge_test_invoices_users_id_users_second_id_foreign'); + $this->assertContains($foreignKeyData[0]->column_name, $haystack); + + $secondIdKey = $this->db->DBDriver === 'Postgre' ? 2 : 1; + $this->assertSame($foreignKeyData[$secondIdKey]->constraint_name, $this->db->DBPrefix . 'forge_test_invoices_users_id_users_second_id_foreign'); + $this->assertContains($foreignKeyData[$secondIdKey]->column_name, $haystack); + } + $this->assertSame($foreignKeyData[0]->table_name, $this->db->DBPrefix . 'forge_test_invoices'); + $this->assertSame($foreignKeyData[0]->foreign_table_name, $this->db->DBPrefix . 'forge_test_users'); + + $this->forge->dropTable('forge_test_invoices', true); + $this->forge->dropTable('forge_test_users', true); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/4310 + */ + public function testCompositeForeignKeyFieldNotExistException() + { + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Field `user_id, user_second_id` not found.'); + + $attributes = []; + + if ($this->db->DBDriver === 'MySQLi') { + $attributes = ['ENGINE' => 'InnoDB']; + } + + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'second_id' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + ]); + $this->forge->addPrimaryKey(['id', 'second_id']); + $this->forge->createTable('forge_test_users', true, $attributes); + + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'users_id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'users_second_id' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addForeignKey(['user_id', 'user_second_id'], 'forge_test_users', ['id', 'second_id'], 'CASCADE', 'CASCADE'); + + $this->forge->createTable('forge_test_invoices', true, $attributes); + } + public function testForeignKeyFieldNotExistException() { $this->expectException(DatabaseException::class); diff --git a/user_guide_src/source/dbmgmt/forge.rst b/user_guide_src/source/dbmgmt/forge.rst index aba92fc634b2..e16b5ee1e92f 100644 --- a/user_guide_src/source/dbmgmt/forge.rst +++ b/user_guide_src/source/dbmgmt/forge.rst @@ -230,13 +230,19 @@ Adding Foreign Keys Foreign Keys help to enforce relationships and actions across your tables. For tables that support Foreign Keys, you may add them directly in forge:: - $forge->addForeignKey('users_id','users','id'); - // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) + $forge->addForeignKey('users_id','users','id'); + // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) + + $forge->addForeignKey(['users_id', 'users_name'],'users',['id', 'name']); + // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) You can specify the desired action for the "on delete" and "on update" properties of the constraint:: - $forge->addForeignKey('users_id','users','id','CASCADE','CASCADE'); - // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE + $forge->addForeignKey('users_id','users','id','CASCADE','CASCADE'); + // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE + + $forge->addForeignKey(['users_id', 'users_name'],'users',['id', 'name'],'CASCADE','CASCADE'); + // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) ON DELETE CASCADE ON UPDATE CASCADE Creating a table ================ From 19de0150cce1291747656314b4cc03fb57e95bd3 Mon Sep 17 00:00:00 2001 From: MonkenWu <610877102@mail.nknu.edu.tw> Date: Fri, 23 Jul 2021 17:33:05 +0800 Subject: [PATCH 067/490] =?UTF-8?q?=EF=BB=BFcreate=20new=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user_guide_src/source/changelogs/v4.2.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/v4.2.0.rst b/user_guide_src/source/changelogs/v4.2.0.rst index c1a7d2673ab1..22a94202a69f 100644 --- a/user_guide_src/source/changelogs/v4.2.0.rst +++ b/user_guide_src/source/changelogs/v4.2.0.rst @@ -8,6 +8,7 @@ Release Date: Not released Enhancements: - Added Cache config for reserved characters +- The ``addForeignKey`` function of the ``Forge`` class can now define composite foreign keys in an array Changes: From 82e9c49092b4fa95f9ec8f3947fa951b64192a3c Mon Sep 17 00:00:00 2001 From: MonkenWu <610877102@mail.nknu.edu.tw> Date: Tue, 27 Jul 2021 22:09:58 +0800 Subject: [PATCH 068/490] fix composition foreign keys exception and add new test case --- system/Database/Forge.php | 2 ++ tests/system/Database/Live/ForgeTest.php | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 39d4384a98ae..cd471138db71 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -398,6 +398,8 @@ public function addForeignKey($fieldName = '', string $tableName = '', $tableFie } if ($errorNames !== []) { + $errorNames[0] = implode(', ',$errorNames); + throw new DatabaseException(lang('Database.fieldNotExists', $errorNames)); } diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index dadcd753e3b5..531f166f2b7d 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -372,6 +372,8 @@ public function testDropTableWithEmptyName() public function testForeignKey() { + $this->forge->dropTable('forge_test_users', true); + $attributes = []; if ($this->db->DBDriver === 'MySQLi') { @@ -639,6 +641,8 @@ public function testForeignKeyFieldNotExistException() public function testDropForeignKey() { + $this->forge->dropTable('forge_test_users', true); + $attributes = []; if ($this->db->DBDriver === 'MySQLi') { From 1734b5ff34596fcc4209ab7bed8524a2ea30ba53 Mon Sep 17 00:00:00 2001 From: MonkenWu <610877102@mail.nknu.edu.tw> Date: Wed, 18 Aug 2021 14:45:31 +0800 Subject: [PATCH 069/490] Modify the parameters in "assertSame" --- tests/system/Database/Live/ForgeTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 531f166f2b7d..d459bc92e391 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -520,21 +520,21 @@ public function testCompositeForeignKey() $foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices'); if ($this->db->DBDriver === 'SQLite3') { - $this->assertSame($foreignKeyData[0]->constraint_name, 'users_id to db_forge_test_users.id'); - $this->assertSame($foreignKeyData[0]->sequence, 0); - $this->assertSame($foreignKeyData[1]->constraint_name, 'users_second_id to db_forge_test_users.second_id'); - $this->assertSame($foreignKeyData[1]->sequence, 1); + $this->assertSame('users_id to db_forge_test_users.id', $foreignKeyData[0]->constraint_name); + $this->assertSame(0, $foreignKeyData[0]->sequence); + $this->assertSame('users_second_id to db_forge_test_users.second_id', $foreignKeyData[1]->constraint_name); + $this->assertSame(1, $foreignKeyData[1]->sequence); } else { $haystack = ['users_id', 'users_second_id']; - $this->assertSame($foreignKeyData[0]->constraint_name, $this->db->DBPrefix . 'forge_test_invoices_users_id_users_second_id_foreign'); + $this->assertSame($this->db->DBPrefix . 'forge_test_invoices_users_id_users_second_id_foreign', $foreignKeyData[0]->constraint_name); $this->assertContains($foreignKeyData[0]->column_name, $haystack); $secondIdKey = $this->db->DBDriver === 'Postgre' ? 2 : 1; - $this->assertSame($foreignKeyData[$secondIdKey]->constraint_name, $this->db->DBPrefix . 'forge_test_invoices_users_id_users_second_id_foreign'); + $this->assertSame($this->db->DBPrefix . 'forge_test_invoices_users_id_users_second_id_foreign', $foreignKeyData[$secondIdKey]->constraint_name); $this->assertContains($foreignKeyData[$secondIdKey]->column_name, $haystack); } - $this->assertSame($foreignKeyData[0]->table_name, $this->db->DBPrefix . 'forge_test_invoices'); - $this->assertSame($foreignKeyData[0]->foreign_table_name, $this->db->DBPrefix . 'forge_test_users'); + $this->assertSame($this->db->DBPrefix . 'forge_test_invoices', $foreignKeyData[0]->table_name); + $this->assertSame($this->db->DBPrefix . 'forge_test_users', $foreignKeyData[0]->foreign_table_name); $this->forge->dropTable('forge_test_invoices', true); $this->forge->dropTable('forge_test_users', true); From eb3ed2b8005c01e43dd9cc8a38bb6d9ef359bf0c Mon Sep 17 00:00:00 2001 From: Mostafa Khudair Date: Fri, 20 Aug 2021 15:17:27 +0200 Subject: [PATCH 070/490] Add missing make:config command config view file --- app/Config/Generators.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Config/Generators.php b/app/Config/Generators.php index c47153d47042..11214fdc6e55 100644 --- a/app/Config/Generators.php +++ b/app/Config/Generators.php @@ -27,6 +27,7 @@ class Generators extends BaseConfig */ public $views = [ 'make:command' => 'CodeIgniter\Commands\Generators\Views\command.tpl.php', + 'make:config' => 'CodeIgniter\Commands\Generators\Views\config.tpl.php', 'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php', 'make:entity' => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php', 'make:filter' => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php', From 86f507621842e53c14fad6917a6642e6a87d13e2 Mon Sep 17 00:00:00 2001 From: Mostafa Khudair <59371810+mostafakhudair@users.noreply.github.com> Date: Tue, 24 Aug 2021 15:25:18 +0200 Subject: [PATCH 071/490] [Userguide] Add missing generator's command (#5019) * Add missing make:validation command * fix shot title --- user_guide_src/source/cli/cli_generators.rst | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/user_guide_src/source/cli/cli_generators.rst b/user_guide_src/source/cli/cli_generators.rst index f7d0ea380fb3..210226b51edc 100644 --- a/user_guide_src/source/cli/cli_generators.rst +++ b/user_guide_src/source/cli/cli_generators.rst @@ -205,6 +205,27 @@ Options: * ``--suffix``: Append the component suffix to the generated class name. * ``--force``: Set this flag to overwrite existing files on destination. +make:validation +--------------- + +Creates a new validation file. + +Usage: +====== +:: + + make:validation [options] + +Argument: +========= +* ``name``: The name of the validation class. **[REQUIRED]** + +Options: +======== +* ``--namespace``: Set the root namespace. Defaults to value of ``APP_NAMESPACE``. +* ``--suffix``: Append the component suffix to the generated class name. +* ``--force``: Set this flag to overwrite existing files on destination. + .. note:: Do you need to have the generated code in a subfolder? Let's say if you want to create a controller class to reside in the ``Admin`` subfolder of the main ``Controllers`` folder, you will just need to prepend the subfolder to the class name, like this: ``php spark make:controller admin/login``. This From bf84d197690de9a3e53df1cf97753cddf485187f Mon Sep 17 00:00:00 2001 From: Mostafa Khudair Date: Thu, 26 Aug 2021 11:21:06 +0200 Subject: [PATCH 072/490] fix make:migration command options --- user_guide_src/source/dbmgmt/migration.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/dbmgmt/migration.rst b/user_guide_src/source/dbmgmt/migration.rst index 9da3e2d3e9b3..88b2a2c1e106 100644 --- a/user_guide_src/source/dbmgmt/migration.rst +++ b/user_guide_src/source/dbmgmt/migration.rst @@ -258,8 +258,11 @@ creates is the Pascal case version of the filename. You can use (make:migration) with the following options: -- ``-n`` - to choose namespace, otherwise the value of ``APP_NAMESPACE`` will be used. -- ``-force`` - If a similarly named migration file is present in destination, this will be overwritten. +- ``--session`` - Generates the migration file for database sessions. +- ``--table`` - Table name to use for database sessions. Default: ``ci_sessions``. +- ``--dbgroup`` - Database group to use for database sessions. Default: ``default``. +- ``--namespace`` - Set root namespace. Default: ``APP_NAMESPACE``. +- ``--suffix`` - Append the component title to the class name. ********************* Migration Preferences From d8c35c179906b529fae60752aec80b80edec43ca Mon Sep 17 00:00:00 2001 From: Mostafa Khudair Date: Wed, 25 Aug 2021 20:33:45 +0200 Subject: [PATCH 073/490] Fix wrong cli command option --- user_guide_src/source/dbmgmt/seeds.rst | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/user_guide_src/source/dbmgmt/seeds.rst b/user_guide_src/source/dbmgmt/seeds.rst index 53a2635ed782..f30bc807ddbb 100644 --- a/user_guide_src/source/dbmgmt/seeds.rst +++ b/user_guide_src/source/dbmgmt/seeds.rst @@ -125,15 +125,13 @@ Using the command line, you can easily generate seed files. :: - // This command will create a UserSeeder seed file - // located at app/Database/Seeds/ directory. - > php spark make:seeder UserSeeder + > php spark make:seeder user --suffix + // Output: UserSeeder.php file located at app/Database/Seeds directory. -You can supply the **root** namespace where the seed file will be stored by supplying the ``-n`` option:: +You can supply the **root** namespace where the seed file will be stored by supplying the ``--namespace`` option:: - > php spark make:seeder MySeeder -n Acme\Blog + > php spark make:seeder MySeeder --namespace Acme\Blog -If ``Acme\Blog`` is mapped to ``app/Blog`` directory, then this command will save the -seed file to ``app/Blog/Database/Seeds/``. +If ``Acme\Blog`` is mapped to ``app/Blog`` directory, then this command will generate ``MySeeder.php`` at ``app/Blog/Database/Seeds`` directory. Supplying the ``--force`` option will overwrite existing files in destination. From 107df929b02548351b8c1d589b855252634b0d23 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Tue, 7 Sep 2021 23:30:27 +0800 Subject: [PATCH 074/490] Fix coding style violations in 4.2 --- app/Config/Publisher.php | 26 +- system/CodeIgniter.php | 2 +- system/Commands/Utilities/Publish.php | 153 ++- system/Common.php | 10 +- system/Config/Publisher.php | 44 +- system/Database/Forge.php | 30 +- system/Database/MySQLi/Connection.php | 2 - system/Database/SQLSRV/Forge.php | 32 +- system/Debug/Toolbar.php | 1 - system/Files/Exceptions/FileException.php | 36 +- system/Files/FileCollection.php | 678 ++++++------ system/Language/en/Files.php | 4 +- system/Language/en/Publisher.php | 20 +- .../Exceptions/PublisherException.php | 60 +- system/Publisher/Publisher.php | 861 ++++++++-------- system/Session/Handlers/DatabaseHandler.php | 2 +- tests/_support/Publishers/TestPublisher.php | 86 +- .../system/Cache/Handlers/BaseHandlerTest.php | 2 +- .../system/Cache/Handlers/FileHandlerTest.php | 2 +- .../Cache/Handlers/MemcachedHandlerTest.php | 2 +- .../Cache/Handlers/PredisHandlerTest.php | 2 +- .../Cache/Handlers/RedisHandlerTest.php | 2 +- .../system/Commands/CommandGeneratorTest.php | 2 +- tests/system/Commands/PublishCommandTest.php | 94 +- tests/system/Files/FileCollectionTest.php | 964 +++++++++--------- tests/system/Language/LanguageTest.php | 8 +- tests/system/Publisher/PublisherInputTest.php | 280 ++--- .../system/Publisher/PublisherOutputTest.php | 440 ++++---- .../Publisher/PublisherRestrictionsTest.php | 200 ++-- .../system/Publisher/PublisherSupportTest.php | 274 ++--- .../Session/Handlers/DatabaseHandlerTest.php | 2 +- tests/system/Validation/ValidationTest.php | 5 - 32 files changed, 2120 insertions(+), 2206 deletions(-) diff --git a/app/Config/Publisher.php b/app/Config/Publisher.php index 2588eea2abac..f3768bc577b4 100644 --- a/app/Config/Publisher.php +++ b/app/Config/Publisher.php @@ -12,17 +12,17 @@ */ class Publisher extends BasePublisher { - /** - * A list of allowed destinations with a (pseudo-)regex - * of allowed files for each destination. - * Attempts to publish to directories not in this list will - * result in a PublisherException. Files that do no fit the - * pattern will cause copy/merge to fail. - * - * @var array - */ - public $restrictions = [ - ROOTPATH => '*', - FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', - ]; + /** + * A list of allowed destinations with a (pseudo-)regex + * of allowed files for each destination. + * Attempts to publish to directories not in this list will + * result in a PublisherException. Files that do no fit the + * pattern will cause copy/merge to fail. + * + * @var array + */ + public $restrictions = [ + ROOTPATH => '*', + FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', + ]; } diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index f18fc38384b6..aee11790f406 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -496,7 +496,7 @@ protected function bootstrapEnvironment() */ protected function startBenchmark() { - if($this->startTime === null) { + if ($this->startTime === null) { $this->startTime = microtime(true); } diff --git a/system/Commands/Utilities/Publish.php b/system/Commands/Utilities/Publish.php index 15448fc90db3..cfed0472c439 100644 --- a/system/Commands/Utilities/Publish.php +++ b/system/Commands/Utilities/Publish.php @@ -1,12 +1,12 @@ * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. */ namespace CodeIgniter\Commands\Utilities; @@ -22,92 +22,83 @@ */ class Publish extends BaseCommand { - /** - * The group the command is lumped under - * when listing commands. - * - * @var string - */ - protected $group = 'CodeIgniter'; + /** + * The group the command is lumped under + * when listing commands. + * + * @var string + */ + protected $group = 'CodeIgniter'; - /** - * The Command's name - * - * @var string - */ - protected $name = 'publish'; + /** + * The Command's name + * + * @var string + */ + protected $name = 'publish'; - /** - * The Command's short description - * - * @var string - */ - protected $description = 'Discovers and executes all predefined Publisher classes.'; + /** + * The Command's short description + * + * @var string + */ + protected $description = 'Discovers and executes all predefined Publisher classes.'; - /** - * The Command's usage - * - * @var string - */ - protected $usage = 'publish []'; + /** + * The Command's usage + * + * @var string + */ + protected $usage = 'publish []'; - /** - * The Command's arguments - * - * @var array - */ - protected $arguments = [ - 'directory' => '[Optional] The directory to scan within each namespace. Default: "Publishers".', - ]; + /** + * The Command's arguments + * + * @var array + */ + protected $arguments = [ + 'directory' => '[Optional] The directory to scan within each namespace. Default: "Publishers".', + ]; - /** - * the Command's Options - * - * @var array - */ - protected $options = []; + /** + * the Command's Options + * + * @var array + */ + protected $options = []; - //-------------------------------------------------------------------- + /** + * Displays the help for the spark cli script itself. + */ + public function run(array $params) + { + $directory = array_shift($params) ?? 'Publishers'; - /** - * Displays the help for the spark cli script itself. - * - * @param array $params - */ - public function run(array $params) - { - $directory = array_shift($params) ?? 'Publishers'; + if ([] === $publishers = Publisher::discover($directory)) { + CLI::write(lang('Publisher.publishMissing', [$directory])); - if ([] === $publishers = Publisher::discover($directory)) - { - CLI::write(lang('Publisher.publishMissing', [$directory])); - return; - } + return; + } - foreach ($publishers as $publisher) - { - if ($publisher->publish()) - { - CLI::write(lang('Publisher.publishSuccess', [ - get_class($publisher), - count($publisher->getPublished()), - $publisher->getDestination(), - ]), 'green'); - } - else - { - CLI::error(lang('Publisher.publishFailure', [ - get_class($publisher), - $publisher->getDestination(), - ]), 'light_gray', 'red'); + foreach ($publishers as $publisher) { + if ($publisher->publish()) { + CLI::write(lang('Publisher.publishSuccess', [ + get_class($publisher), + count($publisher->getPublished()), + $publisher->getDestination(), + ]), 'green'); + } else { + CLI::error(lang('Publisher.publishFailure', [ + get_class($publisher), + $publisher->getDestination(), + ]), 'light_gray', 'red'); - foreach ($publisher->getErrors() as $file => $exception) - { - CLI::write($file); - CLI::error($exception->getMessage()); - CLI::newLine(); - } - } - } - } + foreach ($publisher->getErrors() as $file => $exception) { + CLI::write($file); + CLI::error($exception->getMessage()); + CLI::newLine(); + } + } + } + } } diff --git a/system/Common.php b/system/Common.php index bdea46b26564..9979b30534b2 100644 --- a/system/Common.php +++ b/system/Common.php @@ -719,19 +719,17 @@ function lang(string $line, array $args = [], ?string $locale = null) { $language = Services::language(); - //Get active locale + // Get active locale $activeLocale = $language->getLocale(); - if ($locale && $locale != $activeLocale) - { + if ($locale && $locale !== $activeLocale) { $language->setLocale($locale); } $line = $language->getLine($line, $args); - if ($locale && $locale != $activeLocale) - { - //Reset to active locale + if ($locale && $locale !== $activeLocale) { + // Reset to active locale $language->setLocale($activeLocale); } diff --git a/system/Config/Publisher.php b/system/Config/Publisher.php index 651600ef9c06..608e87afef8e 100644 --- a/system/Config/Publisher.php +++ b/system/Config/Publisher.php @@ -1,12 +1,12 @@ * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. */ namespace CodeIgniter\Config; @@ -19,24 +19,24 @@ */ class Publisher extends BaseConfig { - /** - * A list of allowed destinations with a (pseudo-)regex - * of allowed files for each destination. - * Attempts to publish to directories not in this list will - * result in a PublisherException. Files that do no fit the - * pattern will cause copy/merge to fail. - * - * @var array - */ - public $restrictions = [ - ROOTPATH => '*', - FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', - ]; + /** + * A list of allowed destinations with a (pseudo-)regex + * of allowed files for each destination. + * Attempts to publish to directories not in this list will + * result in a PublisherException. Files that do no fit the + * pattern will cause copy/merge to fail. + * + * @var array + */ + public $restrictions = [ + ROOTPATH => '*', + FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', + ]; - /** - * Disables Registrars to prevent modules from altering the restrictions. - */ - final protected function registerProperties() - { - } + /** + * Disables Registrars to prevent modules from altering the restrictions. + */ + final protected function registerProperties() + { + } } diff --git a/system/Database/Forge.php b/system/Database/Forge.php index cd471138db71..c05b0c760876 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -398,7 +398,7 @@ public function addForeignKey($fieldName = '', string $tableName = '', $tableFie } if ($errorNames !== []) { - $errorNames[0] = implode(', ',$errorNames); + $errorNames[0] = implode(', ', $errorNames); throw new DatabaseException(lang('Database.fieldNotExists', $errorNames)); } @@ -1025,24 +1025,22 @@ protected function _processForeignKeys(string $table): string 'SET DEFAULT', ]; - if ($this->foreignKeys !== []) { - foreach ($this->foreignKeys as $fkey) { - $nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign'; - $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex); - $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field'])); - $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']); - $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField'])); + foreach ($this->foreignKeys as $fkey) { + $nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign'; + $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex); + $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field'])); + $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']); + $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField'])); - $formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)"; - $sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled); + $formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)"; + $sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled); - if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { - $sql .= ' ON DELETE ' . $fkey['onDelete']; - } + if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { + $sql .= ' ON DELETE ' . $fkey['onDelete']; + } - if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) { - $sql .= ' ON UPDATE ' . $fkey['onUpdate']; - } + if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) { + $sql .= ' ON UPDATE ' . $fkey['onUpdate']; } } diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 965d539be878..62881620c46b 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -69,8 +69,6 @@ class Connection extends BaseConnection */ public $resultMode = MYSQLI_STORE_RESULT; - //-------------------------------------------------------------------- - /** * Connect to the database. * diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 3fed6c48962f..ae920ceae152 100755 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -282,24 +282,22 @@ protected function _processForeignKeys(string $table): string $allowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT']; - if ($this->foreignKeys !== []) { - foreach ($this->foreignKeys as $fkey) { - $nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign'; - $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex); - $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field'])); - $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']); - $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField'])); - - $formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)"; - $sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled); - - if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { - $sql .= ' ON DELETE ' . $fkey['onDelete']; - } + foreach ($this->foreignKeys as $fkey) { + $nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign'; + $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex); + $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field'])); + $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']); + $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField'])); + + $formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)"; + $sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled); + + if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { + $sql .= ' ON DELETE ' . $fkey['onDelete']; + } - if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) { - $sql .= ' ON UPDATE ' . $fkey['onUpdate']; - } + if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) { + $sql .= ' ON UPDATE ' . $fkey['onUpdate']; } } diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 6a5ece61a373..ef877cd2f365 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -168,7 +168,6 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques protected function renderTimeline(array $collectors, float $startTime, int $segmentCount, int $segmentDuration, array &$styles): string { $rows = $this->collectTimelineData($collectors); - $output = ''; $styleCount = 0; // Use recursive render function diff --git a/system/Files/Exceptions/FileException.php b/system/Files/Exceptions/FileException.php index 40e9fdc20f41..03af1bc57b08 100644 --- a/system/Files/Exceptions/FileException.php +++ b/system/Files/Exceptions/FileException.php @@ -24,23 +24,23 @@ public static function forUnableToMove(?string $from = null, ?string $to = null, return new static(lang('Files.cannotMove', [$from, $to, $error])); } - /** - * Throws when an item is expected to be a directory but is not or is missing. - * - * @param string $caller The method causing the exception - */ - public static function forExpectedDirectory(string $caller) - { - return new static(lang('Files.expectedDirectory', [$caller])); - } + /** + * Throws when an item is expected to be a directory but is not or is missing. + * + * @param string $caller The method causing the exception + */ + public static function forExpectedDirectory(string $caller) + { + return new static(lang('Files.expectedDirectory', [$caller])); + } - /** - * Throws when an item is expected to be a file but is not or is missing. - * - * @param string $caller The method causing the exception - */ - public static function forExpectedFile(string $caller) - { - return new static(lang('Files.expectedFile', [$caller])); - } + /** + * Throws when an item is expected to be a file but is not or is missing. + * + * @param string $caller The method causing the exception + */ + public static function forExpectedFile(string $caller) + { + return new static(lang('Files.expectedFile', [$caller])); + } } diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index 128d884940d5..0b1714425ba9 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -1,12 +1,12 @@ * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. */ namespace CodeIgniter\Files; @@ -17,7 +17,6 @@ use Generator; use InvalidArgumentException; use IteratorAggregate; -use Traversable; /** * File Collection Class @@ -27,360 +26,324 @@ */ class FileCollection implements Countable, IteratorAggregate { - /** - * The current list of file paths. - * - * @var string[] - */ - protected $files = []; - - //-------------------------------------------------------------------- - // Support Methods - //-------------------------------------------------------------------- - - /** - * Resolves a full path and verifies it is an actual directory. - * - * @param string $directory - * - * @return string - * - * @throws FileException - */ - final protected static function resolveDirectory(string $directory): string - { - if (! is_dir($directory = set_realpath($directory))) - { - $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; - throw FileException::forExpectedDirectory($caller['function']); - } - - return $directory; - } - - /** - * Resolves a full path and verifies it is an actual file. - * - * @param string $file - * - * @return string - * - * @throws FileException - */ - final protected static function resolveFile(string $file): string - { - if (! is_file($file = set_realpath($file))) - { - $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; - throw FileException::forExpectedFile($caller['function']); - } - - return $file; - } - - /** - * Removes files that are not part of the given directory (recursive). - * - * @param string[] $files - * @param string $directory - * - * @return string[] - */ - final protected static function filterFiles(array $files, string $directory): array - { - $directory = self::resolveDirectory($directory); - - return array_filter($files, static function (string $value) use ($directory): bool { - return strpos($value, $directory) === 0; - }); - } - - /** - * Returns any files whose `basename` matches the given pattern. - * - * @param string[] $files - * @param string $pattern Regex or pseudo-regex string - * - * @return string[] - */ - final protected static function matchFiles(array $files, string $pattern): array - { - // Convert pseudo-regex into their true form - if (@preg_match($pattern, '') === false) - { - $pattern = str_replace( - ['#', '.', '*', '?'], - ['\#', '\.', '.*', '.'], - $pattern - ); - $pattern = "#{$pattern}#"; - } - - return array_filter($files, static function ($value) use ($pattern) { - return (bool) preg_match($pattern, basename($value)); - }); - } - - //-------------------------------------------------------------------- - // Class Core - //-------------------------------------------------------------------- - - /** - * Loads the Filesystem helper and adds any initial files. - * - * @param string[] $files - */ - public function __construct(array $files = []) - { - helper(['filesystem']); - - $this->add($files)->define(); - } - - /** - * Applies any initial inputs after the constructor. - * This method is a stub to be implemented by child classes. - * - * @return void - */ - protected function define(): void - { - } - - /** - * Optimizes and returns the current file list. - * - * @return string[] - */ - public function get(): array - { - $this->files = array_unique($this->files); - sort($this->files, SORT_STRING); - - return $this->files; - } - - /** - * Sets the file list directly, files are still subject to verification. - * This works as a "reset" method with []. - * - * @param string[] $files The new file list to use - * - * @return $this - */ - public function set(array $files) - { - $this->files = []; - - return $this->addFiles($files); - } - - /** - * Adds an array/single file or directory to the list. - * - * @param string|string[] $paths - * @param boolean $recursive - * - * @return $this - */ - public function add($paths, bool $recursive = true) - { - $paths = (array) $paths; - - foreach ($paths as $path) - { - if (! is_string($path)) - { - throw new InvalidArgumentException('FileCollection paths must be strings.'); - } - - // Test for a directory - try - { - $directory = self::resolveDirectory($path); - } - catch (FileException $e) - { - return $this->addFile($path); - } - - $this->addDirectory($path, $recursive); - } - - return $this; - } - - //-------------------------------------------------------------------- - // File Handling - //-------------------------------------------------------------------- - - /** - * Verifies and adds files to the list. - * - * @param string[] $files - * - * @return $this - */ - public function addFiles(array $files) - { - foreach ($files as $file) - { - $this->addFile($file); - } - - return $this; - } - - /** - * Verifies and adds a single file to the file list. - * - * @param string $file - * - * @return $this - */ - public function addFile(string $file) - { - $this->files[] = self::resolveFile($file); - - return $this; - } - - /** - * Removes files from the list. - * - * @param string[] $files - * - * @return $this - */ - public function removeFiles(array $files) - { - $this->files = array_diff($this->files, $files); - - return $this; - } - - /** - * Removes a single file from the list. - * - * @param string $file - * - * @return $this - */ - public function removeFile(string $file) - { - return $this->removeFiles([$file]); - } - - //-------------------------------------------------------------------- - // Directory Handling - //-------------------------------------------------------------------- - - /** - * Verifies and adds files from each - * directory to the list. - * - * @param string[] $directories - * @param bool $recursive - * - * @return $this - */ - public function addDirectories(array $directories, bool $recursive = false) - { - foreach ($directories as $directory) - { - $this->addDirectory($directory, $recursive); - } - - return $this; - } - - /** - * Verifies and adds all files from a directory. - * - * @param string $directory - * @param boolean $recursive - * - * @return $this - */ - public function addDirectory(string $directory, bool $recursive = false) - { - $directory = self::resolveDirectory($directory); - - // Map the directory to depth 2 to so directories become arrays - foreach (directory_map($directory, 2, true) as $key => $path) - { - if (is_string($path)) - { - $this->addFile($directory . $path); - } - elseif ($recursive && is_array($path)) - { - $this->addDirectory($directory . $key, true); - } - } - - return $this; - } - - //-------------------------------------------------------------------- - // Filtering - //-------------------------------------------------------------------- - - /** - * Removes any files from the list that match the supplied pattern - * (within the optional scope). - * - * @param string $pattern Regex or pseudo-regex string - * @param string|null $scope The directory to limit the scope - * - * @return $this - */ - public function removePattern(string $pattern, string $scope = null) - { - if ($pattern === '') - { - return $this; - } - - // Start with all files or those in scope - $files = is_null($scope) ? $this->files : self::filterFiles($this->files, $scope); - - // Remove any files that match the pattern - return $this->removeFiles(self::matchFiles($files, $pattern)); - } - - /** - * Keeps only the files from the list that match - * (within the optional scope). - * - * @param string $pattern Regex or pseudo-regex string - * @param string|null $scope A directory to limit the scope - * - * @return $this - */ - public function retainPattern(string $pattern, string $scope = null) - { - if ($pattern === '') - { - return $this; - } - - // Start with all files or those in scope - $files = is_null($scope) ? $this->files : self::filterFiles($this->files, $scope); - - // Matches the pattern within the scoped files and remove their inverse. - return $this->removeFiles(array_diff($files, self::matchFiles($files, $pattern))); - } - - //-------------------------------------------------------------------- - // Interface Methods - //-------------------------------------------------------------------- + /** + * The current list of file paths. + * + * @var string[] + */ + protected $files = []; + + //-------------------------------------------------------------------- + // Support Methods + //-------------------------------------------------------------------- + + /** + * Resolves a full path and verifies it is an actual directory. + * + * @throws FileException + */ + final protected static function resolveDirectory(string $directory): string + { + if (! is_dir($directory = set_realpath($directory))) { + $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + + throw FileException::forExpectedDirectory($caller['function']); + } + + return $directory; + } + + /** + * Resolves a full path and verifies it is an actual file. + * + * @throws FileException + */ + final protected static function resolveFile(string $file): string + { + if (! is_file($file = set_realpath($file))) { + $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + + throw FileException::forExpectedFile($caller['function']); + } + + return $file; + } + + /** + * Removes files that are not part of the given directory (recursive). + * + * @param string[] $files + * + * @return string[] + */ + final protected static function filterFiles(array $files, string $directory): array + { + $directory = self::resolveDirectory($directory); + + return array_filter($files, static function (string $value) use ($directory): bool { + return strpos($value, $directory) === 0; + }); + } + + /** + * Returns any files whose `basename` matches the given pattern. + * + * @param string[] $files + * @param string $pattern Regex or pseudo-regex string + * + * @return string[] + */ + final protected static function matchFiles(array $files, string $pattern): array + { + // Convert pseudo-regex into their true form + if (@preg_match($pattern, '') === false) { + $pattern = str_replace( + ['#', '.', '*', '?'], + ['\#', '\.', '.*', '.'], + $pattern + ); + $pattern = "#{$pattern}#"; + } + + return array_filter($files, static function ($value) use ($pattern) { + return (bool) preg_match($pattern, basename($value)); + }); + } + + //-------------------------------------------------------------------- + // Class Core + //-------------------------------------------------------------------- + + /** + * Loads the Filesystem helper and adds any initial files. + * + * @param string[] $files + */ + public function __construct(array $files = []) + { + helper(['filesystem']); + + $this->add($files)->define(); + } + + /** + * Applies any initial inputs after the constructor. + * This method is a stub to be implemented by child classes. + */ + protected function define(): void + { + } + + /** + * Optimizes and returns the current file list. + * + * @return string[] + */ + public function get(): array + { + $this->files = array_unique($this->files); + sort($this->files, SORT_STRING); + + return $this->files; + } + + /** + * Sets the file list directly, files are still subject to verification. + * This works as a "reset" method with []. + * + * @param string[] $files The new file list to use + * + * @return $this + */ + public function set(array $files) + { + $this->files = []; + + return $this->addFiles($files); + } + + /** + * Adds an array/single file or directory to the list. + * + * @param string|string[] $paths + * + * @return $this + */ + public function add($paths, bool $recursive = true) + { + $paths = (array) $paths; + + foreach ($paths as $path) { + if (! is_string($path)) { + throw new InvalidArgumentException('FileCollection paths must be strings.'); + } + + try { + // Test for a directory + self::resolveDirectory($path); + } catch (FileException $e) { + return $this->addFile($path); + } + + $this->addDirectory($path, $recursive); + } + + return $this; + } + + //-------------------------------------------------------------------- + // File Handling + //-------------------------------------------------------------------- + + /** + * Verifies and adds files to the list. + * + * @param string[] $files + * + * @return $this + */ + public function addFiles(array $files) + { + foreach ($files as $file) { + $this->addFile($file); + } + + return $this; + } + + /** + * Verifies and adds a single file to the file list. + * + * @return $this + */ + public function addFile(string $file) + { + $this->files[] = self::resolveFile($file); + + return $this; + } + + /** + * Removes files from the list. + * + * @param string[] $files + * + * @return $this + */ + public function removeFiles(array $files) + { + $this->files = array_diff($this->files, $files); + + return $this; + } + + /** + * Removes a single file from the list. + * + * @return $this + */ + public function removeFile(string $file) + { + return $this->removeFiles([$file]); + } + + //-------------------------------------------------------------------- + // Directory Handling + //-------------------------------------------------------------------- + + /** + * Verifies and adds files from each + * directory to the list. + * + * @param string[] $directories + * + * @return $this + */ + public function addDirectories(array $directories, bool $recursive = false) + { + foreach ($directories as $directory) { + $this->addDirectory($directory, $recursive); + } + + return $this; + } + + /** + * Verifies and adds all files from a directory. + * + * @return $this + */ + public function addDirectory(string $directory, bool $recursive = false) + { + $directory = self::resolveDirectory($directory); + + // Map the directory to depth 2 to so directories become arrays + foreach (directory_map($directory, 2, true) as $key => $path) { + if (is_string($path)) { + $this->addFile($directory . $path); + } elseif ($recursive && is_array($path)) { + $this->addDirectory($directory . $key, true); + } + } + + return $this; + } + + //-------------------------------------------------------------------- + // Filtering + //-------------------------------------------------------------------- + + /** + * Removes any files from the list that match the supplied pattern + * (within the optional scope). + * + * @param string $pattern Regex or pseudo-regex string + * @param string|null $scope The directory to limit the scope + * + * @return $this + */ + public function removePattern(string $pattern, ?string $scope = null) + { + if ($pattern === '') { + return $this; + } + + // Start with all files or those in scope + $files = $scope === null ? $this->files : self::filterFiles($this->files, $scope); + + // Remove any files that match the pattern + return $this->removeFiles(self::matchFiles($files, $pattern)); + } + + /** + * Keeps only the files from the list that match + * (within the optional scope). + * + * @param string $pattern Regex or pseudo-regex string + * @param string|null $scope A directory to limit the scope + * + * @return $this + */ + public function retainPattern(string $pattern, ?string $scope = null) + { + if ($pattern === '') { + return $this; + } + + // Start with all files or those in scope + $files = $scope === null ? $this->files : self::filterFiles($this->files, $scope); + + // Matches the pattern within the scoped files and remove their inverse. + return $this->removeFiles(array_diff($files, self::matchFiles($files, $pattern))); + } + + //-------------------------------------------------------------------- + // Interface Methods + //-------------------------------------------------------------------- /** * Returns the current number of files in the collection. * Fulfills Countable. - * - * @return int */ public function count(): int { @@ -397,9 +360,8 @@ public function count(): int */ public function getIterator(): Generator { - foreach ($this->get() as $file) - { - yield new File($file, true); - } + foreach ($this->get() as $file) { + yield new File($file, true); + } } } diff --git a/system/Language/en/Files.php b/system/Language/en/Files.php index bcbc39d7e349..03fa776e4822 100644 --- a/system/Language/en/Files.php +++ b/system/Language/en/Files.php @@ -13,6 +13,6 @@ return [ 'fileNotFound' => 'File not found: {0}', 'cannotMove' => 'Could not move file {0} to {1} ({2}).', - 'expectedDirectory' => '{0} expects a valid directory.', - 'expectedFile' => '{0} expects a valid file.', + 'expectedDirectory' => '{0} expects a valid directory.', + 'expectedFile' => '{0} expects a valid file.', ]; diff --git a/system/Language/en/Publisher.php b/system/Language/en/Publisher.php index 77d805a78ff2..f335b98423f7 100644 --- a/system/Language/en/Publisher.php +++ b/system/Language/en/Publisher.php @@ -1,22 +1,22 @@ * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. */ // Publisher language settings return [ - 'collision' => 'Publisher encountered an unexpected {0} while copying {1} to {2}.', - 'destinationNotAllowed' => 'Destination is not on the allowed list of Publisher directories: {0}', - 'fileNotAllowed' => '{0} fails the following restriction for {1}: {2}', + 'collision' => 'Publisher encountered an unexpected {0} while copying {1} to {2}.', + 'destinationNotAllowed' => 'Destination is not on the allowed list of Publisher directories: {0}', + 'fileNotAllowed' => '{0} fails the following restriction for {1}: {2}', - // Publish Command - 'publishMissing' => 'No Publisher classes detected in {0} across all namespaces.', - 'publishSuccess' => '{0} published {1} file(s) to {2}.', - 'publishFailure' => '{0} failed to publish to {1}!', + // Publish Command + 'publishMissing' => 'No Publisher classes detected in {0} across all namespaces.', + 'publishSuccess' => '{0} published {1} file(s) to {2}.', + 'publishFailure' => '{0} failed to publish to {1}!', ]; diff --git a/system/Publisher/Exceptions/PublisherException.php b/system/Publisher/Exceptions/PublisherException.php index c3e85a994ebe..535d3a757d15 100644 --- a/system/Publisher/Exceptions/PublisherException.php +++ b/system/Publisher/Exceptions/PublisherException.php @@ -1,12 +1,12 @@ * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. */ namespace CodeIgniter\Publisher\Exceptions; @@ -20,36 +20,30 @@ */ class PublisherException extends FrameworkException { - /** - * Throws when a file should be overwritten yet cannot. - * - * @param string $from The source file - * @param string $to The destination file - */ - public static function forCollision(string $from, string $to) - { - return new static(lang('Publisher.collision', [filetype($to), $from, $to])); - } + /** + * Throws when a file should be overwritten yet cannot. + * + * @param string $from The source file + * @param string $to The destination file + */ + public static function forCollision(string $from, string $to) + { + return new static(lang('Publisher.collision', [filetype($to), $from, $to])); + } - /** - * Throws when given a destination that is not in the list of allowed directories. - * - * @param string $destination - */ - public static function forDestinationNotAllowed(string $destination) - { - return new static(lang('Publisher.destinationNotAllowed', [$destination])); - } + /** + * Throws when given a destination that is not in the list of allowed directories. + */ + public static function forDestinationNotAllowed(string $destination) + { + return new static(lang('Publisher.destinationNotAllowed', [$destination])); + } - /** - * Throws when a file fails to match the allowed pattern for its destination. - * - * @param string $file - * @param string $directory - * @param string $pattern - */ - public static function forFileNotAllowed(string $file, string $directory, string $pattern) - { - return new static(lang('Publisher.fileNotAllowed', [$file, $directory, $pattern])); - } + /** + * Throws when a file fails to match the allowed pattern for its destination. + */ + public static function forFileNotAllowed(string $file, string $directory, string $pattern) + { + return new static(lang('Publisher.fileNotAllowed', [$file, $directory, $pattern])); + } } diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index ab4484cdea0b..38e990776ef3 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -1,12 +1,12 @@ * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. */ namespace CodeIgniter\Publisher; @@ -19,8 +19,6 @@ use Throwable; /** - * Publisher Class - * * Publishers read in file paths from a variety of sources and copy * the files out to different destinations. This class acts both as * a base for individual publication directives as well as the mode @@ -28,464 +26,413 @@ * path to a verified file while a "path" is relative to its source * or destination and may indicate either a file or directory of * unconfirmed existence. + * * Class failures throw the PublisherException, but some underlying * methods may percolate different exceptions, like FileException, * FileNotFoundException or InvalidArgumentException. + * * Write operations will catch all errors in the file-specific * $errors property to minimize impact of partial batch operations. */ class Publisher extends FileCollection { - /** - * Array of discovered Publishers. - * - * @var array - */ - private static $discovered = []; - - /** - * Directory to use for methods that need temporary storage. - * Created on-the-fly as needed. - * - * @var string|null - */ - private $scratch; - - /** - * Exceptions for specific files from the last write operation. - * - * @var array - */ - private $errors = []; - - /** - * List of file published curing the last write operation. - * - * @var string[] - */ - private $published = []; - - /** - * List of allowed directories and their allowed files regex. - * Restrictions are intentionally private to prevent overriding. - * - * @var array - */ - private $restrictions; - - /** - * Base path to use for the source. - * - * @var string - */ - protected $source = ROOTPATH; - - /** - * Base path to use for the destination. - * - * @var string - */ - protected $destination = FCPATH; - - //-------------------------------------------------------------------- - // Support Methods - //-------------------------------------------------------------------- - - /** - * Discovers and returns all Publishers in the specified namespace directory. - * - * @param string $directory - * - * @return self[] - */ - final public static function discover(string $directory = 'Publishers'): array - { - if (isset(self::$discovered[$directory])) - { - return self::$discovered[$directory]; - } - - self::$discovered[$directory] = []; - - /** @var FileLocator $locator */ - $locator = service('locator'); - - if ([] === $files = $locator->listFiles($directory)) - { - return []; - } - - // Loop over each file checking to see if it is a Publisher - foreach (array_unique($files) as $file) - { - $className = $locator->findQualifiedNameFromPath($file); - - if (is_string($className) && class_exists($className) && is_a($className, self::class, true)) - { - self::$discovered[$directory][] = new $className(); - } - } - - sort(self::$discovered[$directory]); - - return self::$discovered[$directory]; - } - - /* - * Removes a directory and all its files and subdirectories. - * - * @param string $directory - * - * @return void - */ - private static function wipeDirectory(string $directory): void - { - if (is_dir($directory)) - { - // Try a few times in case of lingering locks - $attempts = 10; - while ((bool) $attempts && ! delete_files($directory, true, false, true)) - { - // @codeCoverageIgnoreStart - $attempts--; - usleep(100000); // .1s - // @codeCoverageIgnoreEnd - } - - @rmdir($directory); - } - } - - //-------------------------------------------------------------------- - // Class Core - //-------------------------------------------------------------------- - - /** - * Loads the helper and verifies the source and destination directories. - * - * @param string|null $source - * @param string|null $destination - */ - public function __construct(string $source = null, string $destination = null) - { - helper(['filesystem']); - - $this->source = self::resolveDirectory($source ?? $this->source); - $this->destination = self::resolveDirectory($destination ?? $this->destination); - - // Restrictions are intentionally not injected to prevent overriding - $this->restrictions = config('Publisher')->restrictions; - - // Make sure the destination is allowed - foreach (array_keys($this->restrictions) as $directory) - { - if (strpos($this->destination, $directory) === 0) - { - return; - } - } - - throw PublisherException::forDestinationNotAllowed($this->destination); - } - - /** - * Cleans up any temporary files in the scratch space. - */ - public function __destruct() - { - if (isset($this->scratch)) - { - self::wipeDirectory($this->scratch); - - $this->scratch = null; - } - } - - /** - * Reads files from the sources and copies them out to their destinations. - * This method should be reimplemented by child classes intended for - * discovery. - * - * @return boolean - * - * @throws RuntimeException - */ - public function publish(): bool - { - // Safeguard against accidental misuse - if ($this->source === ROOTPATH && $this->destination === FCPATH) - { - throw new RuntimeException('Child classes of Publisher should provide their own publish method or a source and destination.'); - } - - return $this->addPath('/')->merge(true); - } - - //-------------------------------------------------------------------- - // Property Accessors - //-------------------------------------------------------------------- - - /** - * Returns the source directory. - * - * @return string - */ - final public function getSource(): string - { - return $this->source; - } - - /** - * Returns the destination directory. - * - * @return string - */ - final public function getDestination(): string - { - return $this->destination; - } - - /** - * Returns the temporary workspace, creating it if necessary. - * - * @return string - */ - final public function getScratch(): string - { - if (is_null($this->scratch)) - { - $this->scratch = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)) . DIRECTORY_SEPARATOR; - mkdir($this->scratch, 0700); - } - - return $this->scratch; - } - - /** - * Returns errors from the last write operation if any. - * - * @return array - */ - final public function getErrors(): array - { - return $this->errors; - } - - /** - * Returns the files published by the last write operation. - * - * @return string[] - */ - final public function getPublished(): array - { - return $this->published; - } - - //-------------------------------------------------------------------- - // Additional Handlers - //-------------------------------------------------------------------- - - /** - * Verifies and adds paths to the list. - * - * @param string[] $paths - * @param bool $recursive - * - * @return $this - */ - final public function addPaths(array $paths, bool $recursive = true) - { - foreach ($paths as $path) - { - $this->addPath($path, $recursive); - } - - return $this; - } - - /** - * Adds a single path to the file list. - * - * @param string $path - * @param boolean $recursive - * - * @return $this - */ - final public function addPath(string $path, bool $recursive = true) - { - $this->add($this->source . $path, $recursive); - - return $this; - } - - /** - * Downloads and stages files from an array of URIs. - * - * @param string[] $uris - * - * @return $this - */ - final public function addUris(array $uris) - { - foreach ($uris as $uri) - { - $this->addUri($uri); - } - - return $this; - } - - /** - * Downloads a file from the URI, and adds it to the file list. - * - * @param string $uri Because HTTP\URI is stringable it will still be accepted - * - * @return $this - */ - final public function addUri(string $uri) - { - // Figure out a good filename (using URI strips queries and fragments) - $file = $this->getScratch() . basename((new URI($uri))->getPath()); - - // Get the content and write it to the scratch space - write_file($file, service('curlrequest')->get($uri)->getBody()); - - return $this->addFile($file); - } - - //-------------------------------------------------------------------- - // Write Methods - //-------------------------------------------------------------------- - - /** - * Removes the destination and all its files and folders. - * - * @return $this - */ - final public function wipe() - { - self::wipeDirectory($this->destination); - - return $this; - } - - /** - * Copies all files into the destination, does not create directory structure. - * - * @param boolean $replace Whether to overwrite existing files. - * - * @return boolean Whether all files were copied successfully - */ - final public function copy(bool $replace = true): bool - { - $this->errors = $this->published = []; - - foreach ($this->get() as $file) - { - $to = $this->destination . basename($file); - - try - { - $this->safeCopyFile($file, $to, $replace); - $this->published[] = $to; - } - catch (Throwable $e) - { - $this->errors[$file] = $e; - } - } - - return $this->errors === []; - } - - /** - * Merges all files into the destination. - * Creates a mirrored directory structure only for files from source. - * - * @param boolean $replace Whether to overwrite existing files. - * - * @return boolean Whether all files were copied successfully - */ - final public function merge(bool $replace = true): bool - { - $this->errors = $this->published = []; - - // Get the files from source for special handling - $sourced = self::filterFiles($this->get(), $this->source); - - // Handle everything else with a flat copy - $this->files = array_diff($this->files, $sourced); - $this->copy($replace); - - // Copy each sourced file to its relative destination - foreach ($sourced as $file) - { - // Resolve the destination path - $to = $this->destination . substr($file, strlen($this->source)); - - try - { - $this->safeCopyFile($file, $to, $replace); - $this->published[] = $to; - } - catch (Throwable $e) - { - $this->errors[$file] = $e; - } - } - - return $this->errors === []; - } - - /** - * Copies a file with directory creation and identical file awareness. - * Intentionally allows errors. - * - * @param string $from - * @param string $to - * @param boolean $replace - * - * @return void - * - * @throws PublisherException For collisions and restriction violations - */ - private function safeCopyFile(string $from, string $to, bool $replace): void - { - // Verify this is an allowed file for its destination - foreach ($this->restrictions as $directory => $pattern) - { - if (strpos($to, $directory) === 0 && self::matchFiles([$to], $pattern) === []) - { - throw PublisherException::forFileNotAllowed($from, $directory, $pattern); - } - } - - // Check for an existing file - if (file_exists($to)) - { - // If not replacing or if files are identical then consider successful - if (! $replace || same_file($from, $to)) - { - return; - } - - // If it is a directory then do not try to remove it - if (is_dir($to)) - { - throw PublisherException::forCollision($from, $to); - } - - // Try to remove anything else - unlink($to); - } - - // Make sure the directory exists - if (! is_dir($directory = pathinfo($to, PATHINFO_DIRNAME))) - { - mkdir($directory, 0775, true); - } - - // Allow copy() to throw errors - copy($from, $to); - } + /** + * Array of discovered Publishers. + * + * @var array + */ + private static $discovered = []; + + /** + * Directory to use for methods that need temporary storage. + * Created on-the-fly as needed. + * + * @var string|null + */ + private $scratch; + + /** + * Exceptions for specific files from the last write operation. + * + * @var array + */ + private $errors = []; + + /** + * List of file published curing the last write operation. + * + * @var string[] + */ + private $published = []; + + /** + * List of allowed directories and their allowed files regex. + * Restrictions are intentionally private to prevent overriding. + * + * @var array + */ + private $restrictions; + + /** + * Base path to use for the source. + * + * @var string + */ + protected $source = ROOTPATH; + + /** + * Base path to use for the destination. + * + * @var string + */ + protected $destination = FCPATH; + + //-------------------------------------------------------------------- + // Support Methods + //-------------------------------------------------------------------- + + /** + * Discovers and returns all Publishers in the specified namespace directory. + * + * @return self[] + */ + final public static function discover(string $directory = 'Publishers'): array + { + if (isset(self::$discovered[$directory])) { + return self::$discovered[$directory]; + } + + self::$discovered[$directory] = []; + + /** @var FileLocator $locator */ + $locator = service('locator'); + + if ([] === $files = $locator->listFiles($directory)) { + return []; + } + + // Loop over each file checking to see if it is a Publisher + foreach (array_unique($files) as $file) { + $className = $locator->findQualifiedNameFromPath($file); + + if (is_string($className) && class_exists($className) && is_a($className, self::class, true)) { + self::$discovered[$directory][] = new $className(); + } + } + + sort(self::$discovered[$directory]); + + return self::$discovered[$directory]; + } + + /** + * Removes a directory and all its files and subdirectories. + */ + private static function wipeDirectory(string $directory): void + { + if (is_dir($directory)) { + // Try a few times in case of lingering locks + $attempts = 10; + + while ((bool) $attempts && ! delete_files($directory, true, false, true)) { + // @codeCoverageIgnoreStart + $attempts--; + usleep(100000); // .1s + // @codeCoverageIgnoreEnd + } + + @rmdir($directory); + } + } + + //-------------------------------------------------------------------- + // Class Core + //-------------------------------------------------------------------- + + /** + * Loads the helper and verifies the source and destination directories. + */ + public function __construct(?string $source = null, ?string $destination = null) + { + helper(['filesystem']); + + $this->source = self::resolveDirectory($source ?? $this->source); + $this->destination = self::resolveDirectory($destination ?? $this->destination); + + // Restrictions are intentionally not injected to prevent overriding + $this->restrictions = config('Publisher')->restrictions; + + // Make sure the destination is allowed + foreach (array_keys($this->restrictions) as $directory) { + if (strpos($this->destination, $directory) === 0) { + return; + } + } + + throw PublisherException::forDestinationNotAllowed($this->destination); + } + + /** + * Cleans up any temporary files in the scratch space. + */ + public function __destruct() + { + if (isset($this->scratch)) { + self::wipeDirectory($this->scratch); + + $this->scratch = null; + } + } + + /** + * Reads files from the sources and copies them out to their destinations. + * This method should be reimplemented by child classes intended for + * discovery. + * + * @throws RuntimeException + */ + public function publish(): bool + { + // Safeguard against accidental misuse + if ($this->source === ROOTPATH && $this->destination === FCPATH) { + throw new RuntimeException('Child classes of Publisher should provide their own publish method or a source and destination.'); + } + + return $this->addPath('/')->merge(true); + } + + //-------------------------------------------------------------------- + // Property Accessors + //-------------------------------------------------------------------- + + /** + * Returns the source directory. + */ + final public function getSource(): string + { + return $this->source; + } + + /** + * Returns the destination directory. + */ + final public function getDestination(): string + { + return $this->destination; + } + + /** + * Returns the temporary workspace, creating it if necessary. + */ + final public function getScratch(): string + { + if ($this->scratch === null) { + $this->scratch = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)) . DIRECTORY_SEPARATOR; + mkdir($this->scratch, 0700); + } + + return $this->scratch; + } + + /** + * Returns errors from the last write operation if any. + * + * @return array + */ + final public function getErrors(): array + { + return $this->errors; + } + + /** + * Returns the files published by the last write operation. + * + * @return string[] + */ + final public function getPublished(): array + { + return $this->published; + } + + //-------------------------------------------------------------------- + // Additional Handlers + //-------------------------------------------------------------------- + + /** + * Verifies and adds paths to the list. + * + * @param string[] $paths + * + * @return $this + */ + final public function addPaths(array $paths, bool $recursive = true) + { + foreach ($paths as $path) { + $this->addPath($path, $recursive); + } + + return $this; + } + + /** + * Adds a single path to the file list. + * + * @return $this + */ + final public function addPath(string $path, bool $recursive = true) + { + $this->add($this->source . $path, $recursive); + + return $this; + } + + /** + * Downloads and stages files from an array of URIs. + * + * @param string[] $uris + * + * @return $this + */ + final public function addUris(array $uris) + { + foreach ($uris as $uri) { + $this->addUri($uri); + } + + return $this; + } + + /** + * Downloads a file from the URI, and adds it to the file list. + * + * @param string $uri Because HTTP\URI is stringable it will still be accepted + * + * @return $this + */ + final public function addUri(string $uri) + { + // Figure out a good filename (using URI strips queries and fragments) + $file = $this->getScratch() . basename((new URI($uri))->getPath()); + + // Get the content and write it to the scratch space + write_file($file, service('curlrequest')->get($uri)->getBody()); + + return $this->addFile($file); + } + + //-------------------------------------------------------------------- + // Write Methods + //-------------------------------------------------------------------- + + /** + * Removes the destination and all its files and folders. + * + * @return $this + */ + final public function wipe() + { + self::wipeDirectory($this->destination); + + return $this; + } + + /** + * Copies all files into the destination, does not create directory structure. + * + * @param bool $replace Whether to overwrite existing files. + * + * @return bool Whether all files were copied successfully + */ + final public function copy(bool $replace = true): bool + { + $this->errors = $this->published = []; + + foreach ($this->get() as $file) { + $to = $this->destination . basename($file); + + try { + $this->safeCopyFile($file, $to, $replace); + $this->published[] = $to; + } catch (Throwable $e) { + $this->errors[$file] = $e; + } + } + + return $this->errors === []; + } + + /** + * Merges all files into the destination. + * Creates a mirrored directory structure only for files from source. + * + * @param bool $replace Whether to overwrite existing files. + * + * @return bool Whether all files were copied successfully + */ + final public function merge(bool $replace = true): bool + { + $this->errors = $this->published = []; + + // Get the files from source for special handling + $sourced = self::filterFiles($this->get(), $this->source); + + // Handle everything else with a flat copy + $this->files = array_diff($this->files, $sourced); + $this->copy($replace); + + // Copy each sourced file to its relative destination + foreach ($sourced as $file) { + // Resolve the destination path + $to = $this->destination . substr($file, strlen($this->source)); + + try { + $this->safeCopyFile($file, $to, $replace); + $this->published[] = $to; + } catch (Throwable $e) { + $this->errors[$file] = $e; + } + } + + return $this->errors === []; + } + + /** + * Copies a file with directory creation and identical file awareness. + * Intentionally allows errors. + * + * @throws PublisherException For collisions and restriction violations + */ + private function safeCopyFile(string $from, string $to, bool $replace): void + { + // Verify this is an allowed file for its destination + foreach ($this->restrictions as $directory => $pattern) { + if (strpos($to, $directory) === 0 && self::matchFiles([$to], $pattern) === []) { + throw PublisherException::forFileNotAllowed($from, $directory, $pattern); + } + } + + // Check for an existing file + if (file_exists($to)) { + // If not replacing or if files are identical then consider successful + if (! $replace || same_file($from, $to)) { + return; + } + + // If it is a directory then do not try to remove it + if (is_dir($to)) { + throw PublisherException::forCollision($from, $to); + } + + // Try to remove anything else + unlink($to); + } + + // Make sure the directory exists + if (! is_dir($directory = pathinfo($to, PATHINFO_DIRNAME))) { + mkdir($directory, 0775, true); + } + + // Allow copy() to throw errors + copy($from, $to); + } } diff --git a/system/Session/Handlers/DatabaseHandler.php b/system/Session/Handlers/DatabaseHandler.php index ebfdfc119d5e..f0164d2dfc3c 100644 --- a/system/Session/Handlers/DatabaseHandler.php +++ b/system/Session/Handlers/DatabaseHandler.php @@ -256,7 +256,7 @@ public function gc($max_lifetime) $separator = $this->platform === 'postgre' ? '\'' : ' '; $interval = implode($separator, ['', "{$max_lifetime} second", '']); - return $this->db->table($this->table)->where('timestamp <', "now() - INTERVAL {$interval}", false)->delete() ? true : $this->fail(); + return $this->db->table($this->table)->where('timestamp <', "now() - INTERVAL {$interval}", false)->delete() ? 1 : $this->fail(); } /** diff --git a/tests/_support/Publishers/TestPublisher.php b/tests/_support/Publishers/TestPublisher.php index 47c987a0285a..357e50f17e1a 100644 --- a/tests/_support/Publishers/TestPublisher.php +++ b/tests/_support/Publishers/TestPublisher.php @@ -1,48 +1,56 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace Tests\Support\Publishers; use CodeIgniter\Publisher\Publisher; -use RuntimeException; final class TestPublisher extends Publisher { - /** - * Fakes an error on the given file. - */ - public static function setResult(bool $result) - { - self::$result = $result; - } - - /** - * Return value for publish() - * - * @var boolean - */ - private static $result = true; - - /** - * Base path to use for the source. - * - * @var string - */ - protected $source = SUPPORTPATH . 'Files'; - - /** - * Base path to use for the destination. - * - * @var string - */ - protected $destination = WRITEPATH; - - /** - * Fakes a publish event so no files are actually copied. - */ - public function publish(): bool - { - $this->addPath(''); - - return self::$result; - } + /** + * Fakes an error on the given file. + */ + public static function setResult(bool $result) + { + self::$result = $result; + } + + /** + * Return value for publish() + * + * @var bool + */ + private static $result = true; + + /** + * Base path to use for the source. + * + * @var string + */ + protected $source = SUPPORTPATH . 'Files'; + + /** + * Base path to use for the destination. + * + * @var string + */ + protected $destination = WRITEPATH; + + /** + * Fakes a publish event so no files are actually copied. + */ + public function publish(): bool + { + $this->addPath(''); + + return self::$result; + } } diff --git a/tests/system/Cache/Handlers/BaseHandlerTest.php b/tests/system/Cache/Handlers/BaseHandlerTest.php index 73f46620b559..b2ebb61e01ef 100644 --- a/tests/system/Cache/Handlers/BaseHandlerTest.php +++ b/tests/system/Cache/Handlers/BaseHandlerTest.php @@ -46,7 +46,7 @@ public function invalidTypeProvider(): array public function testValidateKeyUsesConfig() { - config('Cache')->reservedCharacters = 'b'; + config('Cache')->reservedCharacters = 'b'; $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage('Cache key contains reserved characters b'); diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php index 0f9f9a10eadf..5b53a9532b8b 100644 --- a/tests/system/Cache/Handlers/FileHandlerTest.php +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -161,7 +161,7 @@ public function testSavePermanent() $this->assertTrue($this->fileHandler->save(self::$key1, 'value', 0)); $metaData = $this->fileHandler->getMetaData(self::$key1); - $this->assertSame(null, $metaData['expire']); + $this->assertNull($metaData['expire']); $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); $this->assertSame('value', $metaData['data']); diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index 9afbdf742236..63a3994d1756 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -107,7 +107,7 @@ public function testSavePermanent() $this->assertTrue($this->memcachedHandler->save(self::$key1, 'value', 0)); $metaData = $this->memcachedHandler->getMetaData(self::$key1); - $this->assertSame(null, $metaData['expire']); + $this->assertNull($metaData['expire']); $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); $this->assertSame('value', $metaData['data']); diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php index 867babcddc2a..3bcafae48fe2 100644 --- a/tests/system/Cache/Handlers/PredisHandlerTest.php +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php @@ -114,7 +114,7 @@ public function testSavePermanent() $this->assertTrue($this->PredisHandler->save(self::$key1, 'value', 0)); $metaData = $this->PredisHandler->getMetaData(self::$key1); - $this->assertSame(null, $metaData['expire']); + $this->assertNull($metaData['expire']); $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); $this->assertSame('value', $metaData['data']); diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php index b3f8cebbb422..d254cd65af97 100644 --- a/tests/system/Cache/Handlers/RedisHandlerTest.php +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -114,7 +114,7 @@ public function testSavePermanent() $this->assertTrue($this->redisHandler->save(self::$key1, 'value', 0)); $metaData = $this->redisHandler->getMetaData(self::$key1); - $this->assertSame(null, $metaData['expire']); + $this->assertNull($metaData['expire']); $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); $this->assertSame('value', $metaData['data']); diff --git a/tests/system/Commands/CommandGeneratorTest.php b/tests/system/Commands/CommandGeneratorTest.php index 1d5dd6d116c4..c878b969d5e9 100644 --- a/tests/system/Commands/CommandGeneratorTest.php +++ b/tests/system/Commands/CommandGeneratorTest.php @@ -138,7 +138,7 @@ public function testGeneratorPreservesCaseButChangesComponentName(): void */ public function testGeneratorIsNotConfusedWithNamespaceLikeClassNames(): void { - $time = time(); + $time = time(); $notExists = true; command('make:migration App_Lesson'); diff --git a/tests/system/Commands/PublishCommandTest.php b/tests/system/Commands/PublishCommandTest.php index 44f2de25f85a..ea7b09e73511 100644 --- a/tests/system/Commands/PublishCommandTest.php +++ b/tests/system/Commands/PublishCommandTest.php @@ -1,52 +1,64 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace CodeIgniter\Commands; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Filters\CITestStreamFilter; use Tests\Support\Publishers\TestPublisher; +/** + * @internal + */ final class PublishCommandTest extends CIUnitTestCase { - private $streamFilter; - - protected function setUp(): void - { - parent::setUp(); - CITestStreamFilter::$buffer = ''; - - $this->streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); - $this->streamFilter = stream_filter_append(STDERR, 'CITestStreamFilter'); - } - - protected function tearDown(): void - { - parent::tearDown(); - - stream_filter_remove($this->streamFilter); - TestPublisher::setResult(true); - } - - public function testDefault() - { - command('publish'); - - $this->assertStringContainsString(lang('Publisher.publishSuccess', [ - TestPublisher::class, - 0, - WRITEPATH, - ]), CITestStreamFilter::$buffer); - } - - public function testFailure() - { - TestPublisher::setResult(false); - - command('publish'); - - $this->assertStringContainsString(lang('Publisher.publishFailure', [ - TestPublisher::class, - WRITEPATH, - ]), CITestStreamFilter::$buffer); - } + private $streamFilter; + + protected function setUp(): void + { + parent::setUp(); + CITestStreamFilter::$buffer = ''; + + $this->streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); + $this->streamFilter = stream_filter_append(STDERR, 'CITestStreamFilter'); + } + + protected function tearDown(): void + { + parent::tearDown(); + + stream_filter_remove($this->streamFilter); + TestPublisher::setResult(true); + } + + public function testDefault() + { + command('publish'); + + $this->assertStringContainsString(lang('Publisher.publishSuccess', [ + TestPublisher::class, + 0, + WRITEPATH, + ]), CITestStreamFilter::$buffer); + } + + public function testFailure() + { + TestPublisher::setResult(false); + + command('publish'); + + $this->assertStringContainsString(lang('Publisher.publishFailure', [ + TestPublisher::class, + WRITEPATH, + ]), CITestStreamFilter::$buffer); + } } diff --git a/tests/system/Files/FileCollectionTest.php b/tests/system/Files/FileCollectionTest.php index ae2df9178f5f..5621151842e2 100644 --- a/tests/system/Files/FileCollectionTest.php +++ b/tests/system/Files/FileCollectionTest.php @@ -1,568 +1,556 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + use CodeIgniter\Files\Exceptions\FileException; use CodeIgniter\Files\File; use CodeIgniter\Files\FileCollection; use CodeIgniter\Test\CIUnitTestCase; -class FileCollectionTest extends CIUnitTestCase +/** + * @internal + */ +final class FileCollectionTest extends CIUnitTestCase { - /** - * A known, valid file - * - * @var string - */ - private $file = SUPPORTPATH . 'Files/baker/banana.php'; - - /** - * A known, valid directory - * - * @var string - */ - private $directory = SUPPORTPATH . 'Files/able/'; - - /** - * Initialize the helper, since some - * tests call static methods before - * the constructor would load it. - */ - public static function setUpBeforeClass(): void - { - parent::setUpBeforeClass(); - - helper(['filesystem']); - } - - //-------------------------------------------------------------------- - - public function testResolveDirectoryDirectory() - { - $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory'); - - $this->assertSame($this->directory, $method($this->directory)); - } - - public function testResolveDirectoryFile() - { - $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory'); - - $this->expectException(FileException::class); - $this->expectExceptionMessage(lang('Files.expectedDirectory', ['invokeArgs'])); - - $method($this->file); - } - - public function testResolveDirectorySymlink() - { - // Create a symlink to test - $link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4)); - symlink($this->directory, $link); - - $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory'); - - $this->assertSame($this->directory, $method($link)); - - unlink($link); - } - - //-------------------------------------------------------------------- - - public function testResolveFileFile() - { - $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile'); - - $this->assertSame($this->file, $method($this->file)); - } - - public function testResolveFileSymlink() - { - // Create a symlink to test - $link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4)); - symlink($this->file, $link); - - $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile'); - - $this->assertSame($this->file, $method($link)); - - unlink($link); - } - - public function testResolveFileDirectory() - { - $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile'); - - $this->expectException(FileException::class); - $this->expectExceptionMessage(lang('Files.expectedFile', ['invokeArgs'])); - - $method($this->directory); - } - - //-------------------------------------------------------------------- - - public function testConstructorAddsFiles() - { - $expected = [ - $this->directory . 'apple.php', - $this->file, - ]; - - $collection = new class([$this->file]) extends FileCollection { - - protected $files = [ - SUPPORTPATH . 'Files/able/apple.php', - ]; - }; - - $this->assertSame($expected, $collection->get()); - } - - public function testConstructorCallsDefine() - { - $collection = new class() extends FileCollection { - - protected function define(): void - { - $this->add(SUPPORTPATH . 'Files/baker/banana.php'); - } - }; - - $this->assertSame([$this->file], $collection->get()); - } - - //-------------------------------------------------------------------- - - public function testAddStringFile() - { - $files = new FileCollection(); - - $files->add(SUPPORTPATH . 'Files/baker/banana.php'); - - $this->assertSame([$this->file], $files->get()); - } - - public function testAddStringFileRecursiveDoesNothing() - { - $files = new FileCollection(); - - $files->add(SUPPORTPATH . 'Files/baker/banana.php', true); - - $this->assertSame([$this->file], $files->get()); - } - - public function testAddStringDirectory() - { - $files = new FileCollection(); - - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - ]; - - $files->add(SUPPORTPATH . 'Files/able'); - - $this->assertSame($expected, $files->get()); - } - - public function testAddStringDirectoryRecursive() - { - $files = new FileCollection(); - - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; - - $files->add(SUPPORTPATH . 'Files'); + /** + * A known, valid file + * + * @var string + */ + private $file = SUPPORTPATH . 'Files/baker/banana.php'; + + /** + * A known, valid directory + * + * @var string + */ + private $directory = SUPPORTPATH . 'Files/able/'; + + /** + * Initialize the helper, since some + * tests call static methods before + * the constructor would load it. + */ + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); - $this->assertSame($expected, $files->get()); - } + helper(['filesystem']); + } + + public function testResolveDirectoryDirectory() + { + $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory'); - public function testAddArray() - { - $files = new FileCollection(); + $this->assertSame($this->directory, $method($this->directory)); + } - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; + public function testResolveDirectoryFile() + { + $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory'); - $files->add([ - SUPPORTPATH . 'Files/able', - SUPPORTPATH . 'Files/baker/banana.php', - ]); + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedDirectory', ['invokeArgs'])); + + $method($this->file); + } - $this->assertSame($expected, $files->get()); - } + public function testResolveDirectorySymlink() + { + // Create a symlink to test + $link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4)); + symlink($this->directory, $link); - public function testAddArrayRecursive() - { - $files = new FileCollection(); + $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory'); - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - SUPPORTPATH . 'Log/Handlers/TestHandler.php', - ]; + $this->assertSame($this->directory, $method($link)); - $files->add([ - SUPPORTPATH . 'Files', - SUPPORTPATH . 'Log', - ], true); + unlink($link); + } + + public function testResolveFileFile() + { + $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile'); - $this->assertSame($expected, $files->get()); - } + $this->assertSame($this->file, $method($this->file)); + } - //-------------------------------------------------------------------- + public function testResolveFileSymlink() + { + // Create a symlink to test + $link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4)); + symlink($this->file, $link); - public function testAddFile() - { - $collection = new FileCollection(); - $this->assertSame([], $this->getPrivateProperty($collection, 'files')); + $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile'); - $collection->addFile($this->file); - $this->assertSame([$this->file], $this->getPrivateProperty($collection, 'files')); - } + $this->assertSame($this->file, $method($link)); + + unlink($link); + } + + public function testResolveFileDirectory() + { + $method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile'); + + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedFile', ['invokeArgs'])); - public function testAddFileMissing() - { - $collection = new FileCollection(); + $method($this->directory); + } - $this->expectException(FileException::class); - $this->expectExceptionMessage(lang('Files.expectedFile', ['addFile'])); + public function testConstructorAddsFiles() + { + $expected = [ + $this->directory . 'apple.php', + $this->file, + ]; + + $collection = new class ([$this->file]) extends FileCollection { + protected $files = [ + SUPPORTPATH . 'Files/able/apple.php', + ]; + }; + + $this->assertSame($expected, $collection->get()); + } + + public function testConstructorCallsDefine() + { + $collection = new class () extends FileCollection { + protected function define(): void + { + $this->add(SUPPORTPATH . 'Files/baker/banana.php'); + } + }; - $collection->addFile('TheHillsAreAlive.bmp'); - } + $this->assertSame([$this->file], $collection->get()); + } - public function testAddFileDirectory() - { - $collection = new FileCollection(); + public function testAddStringFile() + { + $files = new FileCollection(); - $this->expectException(FileException::class); - $this->expectExceptionMessage(lang('Files.expectedFile', ['addFile'])); + $files->add(SUPPORTPATH . 'Files/baker/banana.php'); - $collection->addFile($this->directory); - } + $this->assertSame([$this->file], $files->get()); + } - public function testAddFiles() - { - $collection = new FileCollection(); - $files = [ - $this->file, - $this->file, - ]; + public function testAddStringFileRecursiveDoesNothing() + { + $files = new FileCollection(); - $collection->addFiles($files); - $this->assertSame($files, $this->getPrivateProperty($collection, 'files')); - } + $files->add(SUPPORTPATH . 'Files/baker/banana.php', true); - //-------------------------------------------------------------------- + $this->assertSame([$this->file], $files->get()); + } - public function testGet() - { - $collection = new FileCollection(); - $collection->addFile($this->file); - - $this->assertSame([$this->file], $collection->get()); - } + public function testAddStringDirectory() + { + $files = new FileCollection(); - public function testGetSorts() - { - $collection = new FileCollection(); - $files = [ - $this->file, - $this->directory . 'apple.php', - ]; - - $collection->addFiles($files); - - $this->assertSame(array_reverse($files), $collection->get()); - } - - public function testGetUniques() - { - $collection = new FileCollection(); - $files = [ - $this->file, - $this->file, - ]; - - $collection->addFiles($files); - $this->assertSame([$this->file], $collection->get()); - } - - public function testSet() - { - $collection = new FileCollection(); - - $collection->set([$this->file]); - $this->assertSame([$this->file], $collection->get()); - } - - public function testSetInvalid() - { - $collection = new FileCollection(); - - $this->expectException(FileException::class); - $this->expectExceptionMessage(lang('Files.expectedFile', ['addFile'])); - - $collection->set(['flerb']); - } - - //-------------------------------------------------------------------- - - public function testRemoveFile() - { - $collection = new FileCollection(); - $files = [ - $this->file, - $this->directory . 'apple.php', - ]; - - $collection->addFiles($files); - - $collection->removeFile($this->file); - - $this->assertSame([$this->directory . 'apple.php'], $collection->get()); - } - - public function testRemoveFiles() - { - $collection = new FileCollection(); - $files = [ - $this->file, - $this->directory . 'apple.php', - ]; - - $collection->addFiles($files); - - $collection->removeFiles($files); - - $this->assertSame([], $collection->get()); - } - - //-------------------------------------------------------------------- - - public function testAddDirectoryInvalid() - { - $collection = new FileCollection(); - - $this->expectException(FileException::class); - $this->expectExceptionMessage(lang('Files.expectedDirectory', ['addDirectory'])); - - $collection->addDirectory($this->file); - } - - public function testAddDirectory() - { - $collection = new FileCollection(); - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - ]; - - $collection->addDirectory($this->directory); - - $this->assertSame($expected, $collection->get()); - } - - public function testAddDirectoryRecursive() - { - $collection = new FileCollection(); - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + ]; - $collection->addDirectory(SUPPORTPATH . 'Files', true); + $files->add(SUPPORTPATH . 'Files/able'); - $this->assertSame($expected, $collection->get()); - } + $this->assertSame($expected, $files->get()); + } - public function testAddDirectories() - { - $collection = new FileCollection(); - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; + public function testAddStringDirectoryRecursive() + { + $files = new FileCollection(); - $collection->addDirectories([ - $this->directory, - SUPPORTPATH . 'Files/baker', - ]); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $files->add(SUPPORTPATH . 'Files'); + + $this->assertSame($expected, $files->get()); + } + + public function testAddArray() + { + $files = new FileCollection(); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $files->add([ + SUPPORTPATH . 'Files/able', + SUPPORTPATH . 'Files/baker/banana.php', + ]); + + $this->assertSame($expected, $files->get()); + } + + public function testAddArrayRecursive() + { + $files = new FileCollection(); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + SUPPORTPATH . 'Log/Handlers/TestHandler.php', + ]; + + $files->add([ + SUPPORTPATH . 'Files', + SUPPORTPATH . 'Log', + ], true); + + $this->assertSame($expected, $files->get()); + } + + public function testAddFile() + { + $collection = new FileCollection(); + $this->assertSame([], $this->getPrivateProperty($collection, 'files')); + + $collection->addFile($this->file); + $this->assertSame([$this->file], $this->getPrivateProperty($collection, 'files')); + } - $this->assertSame($expected, $collection->get()); - } + public function testAddFileMissing() + { + $collection = new FileCollection(); + + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedFile', ['addFile'])); + + $collection->addFile('TheHillsAreAlive.bmp'); + } + + public function testAddFileDirectory() + { + $collection = new FileCollection(); + + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedFile', ['addFile'])); + + $collection->addFile($this->directory); + } + + public function testAddFiles() + { + $collection = new FileCollection(); + $files = [ + $this->file, + $this->file, + ]; + + $collection->addFiles($files); + $this->assertSame($files, $this->getPrivateProperty($collection, 'files')); + } + + public function testGet() + { + $collection = new FileCollection(); + $collection->addFile($this->file); + + $this->assertSame([$this->file], $collection->get()); + } + + public function testGetSorts() + { + $collection = new FileCollection(); + $files = [ + $this->file, + $this->directory . 'apple.php', + ]; + + $collection->addFiles($files); + + $this->assertSame(array_reverse($files), $collection->get()); + } + + public function testGetUniques() + { + $collection = new FileCollection(); + $files = [ + $this->file, + $this->file, + ]; + + $collection->addFiles($files); + $this->assertSame([$this->file], $collection->get()); + } + + public function testSet() + { + $collection = new FileCollection(); + + $collection->set([$this->file]); + $this->assertSame([$this->file], $collection->get()); + } + + public function testSetInvalid() + { + $collection = new FileCollection(); + + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedFile', ['addFile'])); + + $collection->set(['flerb']); + } + + public function testRemoveFile() + { + $collection = new FileCollection(); + $files = [ + $this->file, + $this->directory . 'apple.php', + ]; + + $collection->addFiles($files); + + $collection->removeFile($this->file); + + $this->assertSame([$this->directory . 'apple.php'], $collection->get()); + } + + public function testRemoveFiles() + { + $collection = new FileCollection(); + $files = [ + $this->file, + $this->directory . 'apple.php', + ]; + + $collection->addFiles($files); + + $collection->removeFiles($files); + + $this->assertSame([], $collection->get()); + } + + public function testAddDirectoryInvalid() + { + $collection = new FileCollection(); + + $this->expectException(FileException::class); + $this->expectExceptionMessage(lang('Files.expectedDirectory', ['addDirectory'])); + + $collection->addDirectory($this->file); + } + + public function testAddDirectory() + { + $collection = new FileCollection(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + ]; + + $collection->addDirectory($this->directory); + + $this->assertSame($expected, $collection->get()); + } + + public function testAddDirectoryRecursive() + { + $collection = new FileCollection(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $this->assertSame($expected, $collection->get()); + } + + public function testAddDirectories() + { + $collection = new FileCollection(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; - public function testAddDirectoriesRecursive() - { - $collection = new FileCollection(); - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - SUPPORTPATH . 'Log/Handlers/TestHandler.php', - ]; + $collection->addDirectories([ + $this->directory, + SUPPORTPATH . 'Files/baker', + ]); - $collection->addDirectories([ - SUPPORTPATH . 'Files', - SUPPORTPATH . 'Log', - ], true); + $this->assertSame($expected, $collection->get()); + } - $this->assertSame($expected, $collection->get()); - } + public function testAddDirectoriesRecursive() + { + $collection = new FileCollection(); + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + SUPPORTPATH . 'Log/Handlers/TestHandler.php', + ]; - //-------------------------------------------------------------------- + $collection->addDirectories([ + SUPPORTPATH . 'Files', + SUPPORTPATH . 'Log', + ], true); - public function testRemovePatternEmpty() - { - $collection = new FileCollection(); - $collection->addDirectory(SUPPORTPATH . 'Files', true); + $this->assertSame($expected, $collection->get()); + } - $files = $collection->get(); + public function testRemovePatternEmpty() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); - $collection->removePattern(''); + $files = $collection->get(); - $this->assertSame($files, $collection->get()); - } + $collection->removePattern(''); - public function testRemovePatternRegex() - { - $collection = new FileCollection(); - $collection->addDirectory(SUPPORTPATH . 'Files', true); + $this->assertSame($files, $collection->get()); + } - $expected = [ - $this->directory . 'apple.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; + public function testRemovePatternRegex() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); - $collection->removePattern('#[a-z]+_.*#'); + $expected = [ + $this->directory . 'apple.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; - $this->assertSame($expected, $collection->get()); - } + $collection->removePattern('#[a-z]+_.*#'); - public function testRemovePatternPseudo() - { - $collection = new FileCollection(); - $collection->addDirectory(SUPPORTPATH . 'Files', true); + $this->assertSame($expected, $collection->get()); + } - $expected = [ - $this->directory . 'apple.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; + public function testRemovePatternPseudo() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); - $collection->removePattern('*_*.php'); + $expected = [ + $this->directory . 'apple.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; - $this->assertSame($expected, $collection->get()); - } + $collection->removePattern('*_*.php'); - public function testRemovePatternScope() - { - $collection = new FileCollection(); - $collection->addDirectory(SUPPORTPATH . 'Files', true); + $this->assertSame($expected, $collection->get()); + } - $expected = [ - SUPPORTPATH . 'Files/baker/banana.php', - ]; + public function testRemovePatternScope() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); - $collection->removePattern('*.php', $this->directory); + $expected = [ + SUPPORTPATH . 'Files/baker/banana.php', + ]; - $this->assertSame($expected, $collection->get()); - } + $collection->removePattern('*.php', $this->directory); - //-------------------------------------------------------------------- + $this->assertSame($expected, $collection->get()); + } - public function testRetainPatternEmpty() - { - $collection = new FileCollection(); - $collection->addDirectory(SUPPORTPATH . 'Files', true); + public function testRetainPatternEmpty() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); - $files = $collection->get(); + $files = $collection->get(); - $collection->retainPattern(''); + $collection->retainPattern(''); - $this->assertSame($files, $collection->get()); - } + $this->assertSame($files, $collection->get()); + } - public function testRetainPatternRegex() - { - $collection = new FileCollection(); - $collection->addDirectory(SUPPORTPATH . 'Files', true); + public function testRetainPatternRegex() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); - $expected = [ - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - ]; + $expected = [ + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + ]; - $collection->retainPattern('#[a-z]+_.*#'); + $collection->retainPattern('#[a-z]+_.*#'); - $this->assertSame($expected, $collection->get()); - } + $this->assertSame($expected, $collection->get()); + } - public function testRetainPatternPseudo() - { - $collection = new FileCollection(); - $collection->addDirectory(SUPPORTPATH . 'Files', true); + public function testRetainPatternPseudo() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); - $expected = [ - $this->directory . 'fig_3.php', - ]; + $expected = [ + $this->directory . 'fig_3.php', + ]; - $collection->retainPattern('*_?.php'); + $collection->retainPattern('*_?.php'); - $this->assertSame($expected, $collection->get()); - } + $this->assertSame($expected, $collection->get()); + } - public function testRetainPatternScope() - { - $collection = new FileCollection(); - $collection->addDirectory(SUPPORTPATH . 'Files', true); + public function testRetainPatternScope() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); - $expected = [ - $this->directory . 'fig_3.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; + $expected = [ + $this->directory . 'fig_3.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; - $collection->retainPattern('*_?.php', $this->directory); + $collection->retainPattern('*_?.php', $this->directory); - $this->assertSame($expected, $collection->get()); - } + $this->assertSame($expected, $collection->get()); + } - //-------------------------------------------------------------------- + public function testCount() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); - public function testCount() - { - $collection = new FileCollection(); - $collection->addDirectory(SUPPORTPATH . 'Files', true); + $this->assertCount(4, $collection); + } - $this->assertCount(4, $collection); - } + public function testIterable() + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); - public function testIterable() - { - $collection = new FileCollection(); - $collection->addDirectory(SUPPORTPATH . 'Files', true); + $count = 0; - $count = 0; - foreach ($collection as $file) - { - $this->assertInstanceOf(File::class, $file); - $count++; - } + foreach ($collection as $file) { + $this->assertInstanceOf(File::class, $file); + $count++; + } - $this->assertSame($count, 4); - } + $this->assertSame($count, 4); + } } diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php index ace99172aac4..fcf4beea6257 100644 --- a/tests/system/Language/LanguageTest.php +++ b/tests/system/Language/LanguageTest.php @@ -279,17 +279,15 @@ public function testLangKeepLocale() $this->lang = Services::language('en', true); lang('Language.languageGetLineInvalidArgumentException'); - $this->assertEquals('en', $this->lang->getLocale()); + $this->assertSame('en', $this->lang->getLocale()); lang('Language.languageGetLineInvalidArgumentException', [], 'ru'); - $this->assertEquals('en', $this->lang->getLocale()); + $this->assertSame('en', $this->lang->getLocale()); lang('Language.languageGetLineInvalidArgumentException'); - $this->assertEquals('en', $this->lang->getLocale()); + $this->assertSame('en', $this->lang->getLocale()); } - //-------------------------------------------------------------------- - /** * Testing base locale vs variants, with fallback to English. * diff --git a/tests/system/Publisher/PublisherInputTest.php b/tests/system/Publisher/PublisherInputTest.php index 29d48ef78816..a6f6b8eec582 100644 --- a/tests/system/Publisher/PublisherInputTest.php +++ b/tests/system/Publisher/PublisherInputTest.php @@ -1,150 +1,156 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + use CodeIgniter\Publisher\Publisher; use CodeIgniter\Test\CIUnitTestCase; -use Tests\Support\Publishers\TestPublisher; -class PublisherInputTest extends CIUnitTestCase +/** + * @internal + */ +final class PublisherInputTest extends CIUnitTestCase { - /** - * A known, valid file - * - * @var string - */ - private $file = SUPPORTPATH . 'Files/baker/banana.php'; - - /** - * A known, valid directory - * - * @var string - */ - private $directory = SUPPORTPATH . 'Files/able/'; - - /** - * Initialize the helper, since some - * tests call static methods before - * the constructor would load it. - */ - public static function setUpBeforeClass(): void - { - parent::setUpBeforeClass(); - - helper(['filesystem']); - } - - //-------------------------------------------------------------------- - - public function testAddPathFile() - { - $publisher = new Publisher(SUPPORTPATH . 'Files'); - - $publisher->addPath('baker/banana.php'); - - $this->assertSame([$this->file], $publisher->get()); - } - - public function testAddPathFileRecursiveDoesNothing() - { - $publisher = new Publisher(SUPPORTPATH . 'Files'); - - $publisher->addPath('baker/banana.php', true); - - $this->assertSame([$this->file], $publisher->get()); - } - - public function testAddPathDirectory() - { - $publisher = new Publisher(SUPPORTPATH . 'Files'); - - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - ]; - - $publisher->addPath('able'); - - $this->assertSame($expected, $publisher->get()); - } - - public function testAddPathDirectoryRecursive() - { - $publisher = new Publisher(SUPPORTPATH); - - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; - - $publisher->addPath('Files'); - - $this->assertSame($expected, $publisher->get()); - } - - public function testAddPaths() - { - $publisher = new Publisher(SUPPORTPATH . 'Files'); - - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - ]; - - $publisher->addPaths([ - 'able', - 'baker/banana.php', - ]); - - $this->assertSame($expected, $publisher->get()); - } - - public function testAddPathsRecursive() - { - $publisher = new Publisher(SUPPORTPATH); - - $expected = [ - $this->directory . 'apple.php', - $this->directory . 'fig_3.php', - $this->directory . 'prune_ripe.php', - SUPPORTPATH . 'Files/baker/banana.php', - SUPPORTPATH . 'Log/Handlers/TestHandler.php', - ]; - - $publisher->addPaths([ - 'Files', - 'Log', - ], true); - - $this->assertSame($expected, $publisher->get()); - } - - //-------------------------------------------------------------------- + /** + * A known, valid file + * + * @var string + */ + private $file = SUPPORTPATH . 'Files/baker/banana.php'; + + /** + * A known, valid directory + * + * @var string + */ + private $directory = SUPPORTPATH . 'Files/able/'; + + /** + * Initialize the helper, since some + * tests call static methods before + * the constructor would load it. + */ + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + helper(['filesystem']); + } + + public function testAddPathFile() + { + $publisher = new Publisher(SUPPORTPATH . 'Files'); + + $publisher->addPath('baker/banana.php'); + + $this->assertSame([$this->file], $publisher->get()); + } + + public function testAddPathFileRecursiveDoesNothing() + { + $publisher = new Publisher(SUPPORTPATH . 'Files'); + + $publisher->addPath('baker/banana.php', true); + + $this->assertSame([$this->file], $publisher->get()); + } + + public function testAddPathDirectory() + { + $publisher = new Publisher(SUPPORTPATH . 'Files'); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + ]; + + $publisher->addPath('able'); + + $this->assertSame($expected, $publisher->get()); + } + + public function testAddPathDirectoryRecursive() + { + $publisher = new Publisher(SUPPORTPATH); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $publisher->addPath('Files'); + + $this->assertSame($expected, $publisher->get()); + } + + public function testAddPaths() + { + $publisher = new Publisher(SUPPORTPATH . 'Files'); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $publisher->addPaths([ + 'able', + 'baker/banana.php', + ]); + + $this->assertSame($expected, $publisher->get()); + } + + public function testAddPathsRecursive() + { + $publisher = new Publisher(SUPPORTPATH); + + $expected = [ + $this->directory . 'apple.php', + $this->directory . 'fig_3.php', + $this->directory . 'prune_ripe.php', + SUPPORTPATH . 'Files/baker/banana.php', + SUPPORTPATH . 'Log/Handlers/TestHandler.php', + ]; + + $publisher->addPaths([ + 'Files', + 'Log', + ], true); + + $this->assertSame($expected, $publisher->get()); + } - public function testAddUri() - { - $publisher = new Publisher(); - $publisher->addUri('https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/composer.json'); + public function testAddUri() + { + $publisher = new Publisher(); + $publisher->addUri('https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/composer.json'); - $scratch = $this->getPrivateProperty($publisher, 'scratch'); + $scratch = $this->getPrivateProperty($publisher, 'scratch'); - $this->assertSame([$scratch . 'composer.json'], $publisher->get()); - } + $this->assertSame([$scratch . 'composer.json'], $publisher->get()); + } - public function testAddUris() - { - $publisher = new Publisher(); - $publisher->addUris([ - 'https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/LICENSE', - 'https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/composer.json', - ]); + public function testAddUris() + { + $publisher = new Publisher(); + $publisher->addUris([ + 'https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/LICENSE', + 'https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/composer.json', + ]); - $scratch = $this->getPrivateProperty($publisher, 'scratch'); + $scratch = $this->getPrivateProperty($publisher, 'scratch'); - $this->assertSame([$scratch . 'LICENSE', $scratch . 'composer.json'], $publisher->get()); - } + $this->assertSame([$scratch . 'LICENSE', $scratch . 'composer.json'], $publisher->get()); + } } diff --git a/tests/system/Publisher/PublisherOutputTest.php b/tests/system/Publisher/PublisherOutputTest.php index 489058a1c099..512e051f7663 100644 --- a/tests/system/Publisher/PublisherOutputTest.php +++ b/tests/system/Publisher/PublisherOutputTest.php @@ -1,227 +1,231 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + use CodeIgniter\Publisher\Publisher; use CodeIgniter\Test\CIUnitTestCase; -use Tests\Support\Publishers\TestPublisher; use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamDirectory; -class PublisherOutputTest extends CIUnitTestCase +/** + * @internal + */ +final class PublisherOutputTest extends CIUnitTestCase { - /** - * Files to seed to VFS - * - * @var array - */ - private $structure; - - /** - * Virtual destination - * - * @var vfsStreamDirectory - */ - private $root; - - /** - * A known, valid file - * - * @var string - */ - private $file = SUPPORTPATH . 'Files/baker/banana.php'; - - /** - * A known, valid directory - * - * @var string - */ - private $directory = SUPPORTPATH . 'Files/able/'; - - /** - * Initialize the helper, since some - * tests call static methods before - * the constructor would load it. - */ - public static function setUpBeforeClass(): void - { - parent::setUpBeforeClass(); - - helper(['filesystem']); - } - - protected function setUp(): void - { - parent::setUp(); - - $this->structure = [ - 'able' => [ - 'apple.php' => 'Once upon a midnight dreary', - 'bazam' => 'While I pondered weak and weary', - ], - 'boo' => [ - 'far' => 'Upon a tome of long-forgotten lore', - 'faz' => 'There came a tapping up on the door', - ], - 'AnEmptyFolder' => [], - 'simpleFile' => 'A tap-tap-tapping upon my door', - '.hidden' => 'There is no spoon', - ]; - - $this->root = vfsStream::setup('root', null, $this->structure); - - // Add root to the list of allowed destinations - config('Publisher')->restrictions[$this->root->url()] = '*'; - } - - //-------------------------------------------------------------------- - - public function testCopy() - { - $publisher = new Publisher($this->directory, $this->root->url()); - $publisher->addFile($this->file); - - $this->assertFileDoesNotExist($this->root->url() . '/banana.php'); - - $result = $publisher->copy(false); - - $this->assertTrue($result); - $this->assertFileExists($this->root->url() . '/banana.php'); - } - - public function testCopyReplace() - { - $file = $this->directory . 'apple.php'; - $publisher = new Publisher($this->directory, $this->root->url() . '/able'); - $publisher->addFile($file); - - $this->assertFileExists($this->root->url() . '/able/apple.php'); - $this->assertFalse(same_file($file, $this->root->url() . '/able/apple.php')); - - $result = $publisher->copy(true); - - $this->assertTrue($result); - $this->assertTrue(same_file($file, $this->root->url() . '/able/apple.php')); - } - - public function testCopyIgnoresSame() - { - $publisher = new Publisher($this->directory, $this->root->url()); - $publisher->addFile($this->file); - - copy($this->file, $this->root->url() . '/banana.php'); - - $result = $publisher->copy(false); - $this->assertTrue($result); - - $result = $publisher->copy(true); - $this->assertTrue($result); - $this->assertSame([$this->root->url() . '/banana.php'], $publisher->getPublished()); - } - - public function testCopyIgnoresCollision() - { - $publisher = new Publisher($this->directory, $this->root->url()); - - mkdir($this->root->url() . '/banana.php'); - - $result = $publisher->addFile($this->file)->copy(false); - - $this->assertTrue($result); - $this->assertSame([], $publisher->getErrors()); - $this->assertSame([$this->root->url() . '/banana.php'], $publisher->getPublished()); - } - - public function testCopyCollides() - { - $publisher = new Publisher($this->directory, $this->root->url()); - $expected = lang('Publisher.collision', ['dir', $this->file, $this->root->url() . '/banana.php']); - - mkdir($this->root->url() . '/banana.php'); - - $result = $publisher->addFile($this->file)->copy(true); - $errors = $publisher->getErrors(); - - $this->assertFalse($result); - $this->assertCount(1, $errors); - $this->assertSame([$this->file], array_keys($errors)); - $this->assertSame([], $publisher->getPublished()); - $this->assertSame($expected, $errors[$this->file]->getMessage()); - } - - //-------------------------------------------------------------------- - - public function testMerge() - { - $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); - $expected = [ - $this->root->url() . '/able/apple.php', - $this->root->url() . '/able/fig_3.php', - $this->root->url() . '/able/prune_ripe.php', - $this->root->url() . '/baker/banana.php', - ]; - - $this->assertFileDoesNotExist($this->root->url() . '/able/fig_3.php'); - $this->assertDirectoryDoesNotExist($this->root->url() . '/baker'); - - $result = $publisher->addPath('/')->merge(false); - - $this->assertTrue($result); - $this->assertFileExists($this->root->url() . '/able/fig_3.php'); - $this->assertDirectoryExists($this->root->url() . '/baker'); - $this->assertSame($expected, $publisher->getPublished()); - } - - public function testMergeReplace() - { - $this->assertFalse(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php')); - $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); - $expected = [ - $this->root->url() . '/able/apple.php', - $this->root->url() . '/able/fig_3.php', - $this->root->url() . '/able/prune_ripe.php', - $this->root->url() . '/baker/banana.php', - ]; - - $result = $publisher->addPath('/')->merge(true); - - $this->assertTrue($result); - $this->assertTrue(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php')); - $this->assertSame($expected, $publisher->getPublished()); - } - - public function testMergeCollides() - { - $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); - $expected = lang('Publisher.collision', ['dir', $this->directory . 'fig_3.php', $this->root->url() . '/able/fig_3.php']); - $published = [ - $this->root->url() . '/able/apple.php', - $this->root->url() . '/able/prune_ripe.php', - $this->root->url() . '/baker/banana.php', - ]; - - mkdir($this->root->url() . '/able/fig_3.php'); - - $result = $publisher->addPath('/')->merge(true); - $errors = $publisher->getErrors(); - - $this->assertFalse($result); - $this->assertCount(1, $errors); - $this->assertSame([$this->directory . 'fig_3.php'], array_keys($errors)); - $this->assertSame($published, $publisher->getPublished()); - $this->assertSame($expected, $errors[$this->directory . 'fig_3.php']->getMessage()); - } - - //-------------------------------------------------------------------- - - public function testPublish() - { - $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); - - $result = $publisher->publish(); - - $this->assertTrue($result); - $this->assertFileExists($this->root->url() . '/able/fig_3.php'); - $this->assertDirectoryExists($this->root->url() . '/baker'); - $this->assertTrue(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php')); - } + /** + * Files to seed to VFS + * + * @var array + */ + private $structure; + + /** + * Virtual destination + * + * @var vfsStreamDirectory + */ + private $root; + + /** + * A known, valid file + * + * @var string + */ + private $file = SUPPORTPATH . 'Files/baker/banana.php'; + + /** + * A known, valid directory + * + * @var string + */ + private $directory = SUPPORTPATH . 'Files/able/'; + + /** + * Initialize the helper, since some + * tests call static methods before + * the constructor would load it. + */ + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + helper(['filesystem']); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->structure = [ + 'able' => [ + 'apple.php' => 'Once upon a midnight dreary', + 'bazam' => 'While I pondered weak and weary', + ], + 'boo' => [ + 'far' => 'Upon a tome of long-forgotten lore', + 'faz' => 'There came a tapping up on the door', + ], + 'AnEmptyFolder' => [], + 'simpleFile' => 'A tap-tap-tapping upon my door', + '.hidden' => 'There is no spoon', + ]; + + $this->root = vfsStream::setup('root', null, $this->structure); + + // Add root to the list of allowed destinations + config('Publisher')->restrictions[$this->root->url()] = '*'; + } + + public function testCopy() + { + $publisher = new Publisher($this->directory, $this->root->url()); + $publisher->addFile($this->file); + + $this->assertFileDoesNotExist($this->root->url() . '/banana.php'); + + $result = $publisher->copy(false); + + $this->assertTrue($result); + $this->assertFileExists($this->root->url() . '/banana.php'); + } + + public function testCopyReplace() + { + $file = $this->directory . 'apple.php'; + $publisher = new Publisher($this->directory, $this->root->url() . '/able'); + $publisher->addFile($file); + + $this->assertFileExists($this->root->url() . '/able/apple.php'); + $this->assertFalse(same_file($file, $this->root->url() . '/able/apple.php')); + + $result = $publisher->copy(true); + + $this->assertTrue($result); + $this->assertTrue(same_file($file, $this->root->url() . '/able/apple.php')); + } + + public function testCopyIgnoresSame() + { + $publisher = new Publisher($this->directory, $this->root->url()); + $publisher->addFile($this->file); + + copy($this->file, $this->root->url() . '/banana.php'); + + $result = $publisher->copy(false); + $this->assertTrue($result); + + $result = $publisher->copy(true); + $this->assertTrue($result); + $this->assertSame([$this->root->url() . '/banana.php'], $publisher->getPublished()); + } + + public function testCopyIgnoresCollision() + { + $publisher = new Publisher($this->directory, $this->root->url()); + + mkdir($this->root->url() . '/banana.php'); + + $result = $publisher->addFile($this->file)->copy(false); + + $this->assertTrue($result); + $this->assertSame([], $publisher->getErrors()); + $this->assertSame([$this->root->url() . '/banana.php'], $publisher->getPublished()); + } + + public function testCopyCollides() + { + $publisher = new Publisher($this->directory, $this->root->url()); + $expected = lang('Publisher.collision', ['dir', $this->file, $this->root->url() . '/banana.php']); + + mkdir($this->root->url() . '/banana.php'); + + $result = $publisher->addFile($this->file)->copy(true); + $errors = $publisher->getErrors(); + + $this->assertFalse($result); + $this->assertCount(1, $errors); + $this->assertSame([$this->file], array_keys($errors)); + $this->assertSame([], $publisher->getPublished()); + $this->assertSame($expected, $errors[$this->file]->getMessage()); + } + + public function testMerge() + { + $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); + $expected = [ + $this->root->url() . '/able/apple.php', + $this->root->url() . '/able/fig_3.php', + $this->root->url() . '/able/prune_ripe.php', + $this->root->url() . '/baker/banana.php', + ]; + + $this->assertFileDoesNotExist($this->root->url() . '/able/fig_3.php'); + $this->assertDirectoryDoesNotExist($this->root->url() . '/baker'); + + $result = $publisher->addPath('/')->merge(false); + + $this->assertTrue($result); + $this->assertFileExists($this->root->url() . '/able/fig_3.php'); + $this->assertDirectoryExists($this->root->url() . '/baker'); + $this->assertSame($expected, $publisher->getPublished()); + } + + public function testMergeReplace() + { + $this->assertFalse(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php')); + $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); + $expected = [ + $this->root->url() . '/able/apple.php', + $this->root->url() . '/able/fig_3.php', + $this->root->url() . '/able/prune_ripe.php', + $this->root->url() . '/baker/banana.php', + ]; + + $result = $publisher->addPath('/')->merge(true); + + $this->assertTrue($result); + $this->assertTrue(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php')); + $this->assertSame($expected, $publisher->getPublished()); + } + + public function testMergeCollides() + { + $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); + $expected = lang('Publisher.collision', ['dir', $this->directory . 'fig_3.php', $this->root->url() . '/able/fig_3.php']); + $published = [ + $this->root->url() . '/able/apple.php', + $this->root->url() . '/able/prune_ripe.php', + $this->root->url() . '/baker/banana.php', + ]; + + mkdir($this->root->url() . '/able/fig_3.php'); + + $result = $publisher->addPath('/')->merge(true); + $errors = $publisher->getErrors(); + + $this->assertFalse($result); + $this->assertCount(1, $errors); + $this->assertSame([$this->directory . 'fig_3.php'], array_keys($errors)); + $this->assertSame($published, $publisher->getPublished()); + $this->assertSame($expected, $errors[$this->directory . 'fig_3.php']->getMessage()); + } + + public function testPublish() + { + $publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url()); + + $result = $publisher->publish(); + + $this->assertTrue($result); + $this->assertFileExists($this->root->url() . '/able/fig_3.php'); + $this->assertDirectoryExists($this->root->url() . '/baker'); + $this->assertTrue(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php')); + } } diff --git a/tests/system/Publisher/PublisherRestrictionsTest.php b/tests/system/Publisher/PublisherRestrictionsTest.php index 27db2e9429a9..692e49d7680b 100644 --- a/tests/system/Publisher/PublisherRestrictionsTest.php +++ b/tests/system/Publisher/PublisherRestrictionsTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + use CodeIgniter\Publisher\Exceptions\PublisherException; use CodeIgniter\Publisher\Publisher; use CodeIgniter\Test\CIUnitTestCase; @@ -9,100 +18,103 @@ * * Tests that the restrictions defined in the configuration * file properly prevent disallowed actions. + * + * @internal */ -class PublisherRestrictionsTest extends CIUnitTestCase +final class PublisherRestrictionsTest extends CIUnitTestCase { - /** - * @see Tests\Support\Config\Registrars::Publisher() - */ - public function testRegistrarsNotAllowed() - { - $this->assertArrayNotHasKey(SUPPORTPATH, config('Publisher')->restrictions); - } - - public function testImmutableRestrictions() - { - $publisher = new Publisher(); - - // Try to "hack" the Publisher by adding our desired destination to the config - config('Publisher')->restrictions[SUPPORTPATH] = '*'; - - $restrictions = $this->getPrivateProperty($publisher, 'restrictions'); - - $this->assertArrayNotHasKey(SUPPORTPATH, $restrictions); - } - - /** - * @dataProvider fileProvider - */ - public function testDefaultPublicRestrictions(string $path) - { - $publisher = new Publisher(ROOTPATH, FCPATH); - $pattern = config('Publisher')->restrictions[FCPATH]; - - // Use the scratch space to create a file - $file = $publisher->getScratch() . $path; - file_put_contents($file, 'To infinity and beyond!'); - - $result = $publisher->addFile($file)->merge(); - $this->assertFalse($result); - - $errors = $publisher->getErrors(); - $this->assertCount(1, $errors); - $this->assertSame([$file], array_keys($errors)); - - $expected = lang('Publisher.fileNotAllowed', [$file, FCPATH, $pattern]); - $this->assertSame($expected, $errors[$file]->getMessage()); - } - - public function fileProvider() - { - yield 'php' => ['index.php']; - yield 'exe' => ['cat.exe']; - yield 'flat' => ['banana']; - } - - /** - * @dataProvider destinationProvider - */ - public function testDestinations(string $destination, bool $allowed) - { - config('Publisher')->restrictions = [ - APPPATH => '', - FCPATH => '', - SUPPORTPATH . 'Files' => '', - SUPPORTPATH . 'Files/../' => '', - ]; - - if (! $allowed) - { - $this->expectException(PublisherException::class); - $this->expectExceptionMessage(lang('Publisher.destinationNotAllowed', [$destination])); - } - - $publisher = new Publisher(null, $destination); - $this->assertInstanceOf(Publisher::class, $publisher); - } - - public function destinationProvider() - { - return [ - 'explicit' => [ - APPPATH, - true, - ], - 'subdirectory' => [ - APPPATH . 'Config', - true, - ], - 'relative' => [ - SUPPORTPATH . 'Files/able/../', - true, - ], - 'parent' => [ - SUPPORTPATH, - false, - ], - ]; - } + /** + * @see Tests\Support\Config\Registrars::Publisher() + */ + public function testRegistrarsNotAllowed() + { + $this->assertArrayNotHasKey(SUPPORTPATH, config('Publisher')->restrictions); + } + + public function testImmutableRestrictions() + { + $publisher = new Publisher(); + + // Try to "hack" the Publisher by adding our desired destination to the config + config('Publisher')->restrictions[SUPPORTPATH] = '*'; + + $restrictions = $this->getPrivateProperty($publisher, 'restrictions'); + + $this->assertArrayNotHasKey(SUPPORTPATH, $restrictions); + } + + /** + * @dataProvider fileProvider + */ + public function testDefaultPublicRestrictions(string $path) + { + $publisher = new Publisher(ROOTPATH, FCPATH); + $pattern = config('Publisher')->restrictions[FCPATH]; + + // Use the scratch space to create a file + $file = $publisher->getScratch() . $path; + file_put_contents($file, 'To infinity and beyond!'); + + $result = $publisher->addFile($file)->merge(); + $this->assertFalse($result); + + $errors = $publisher->getErrors(); + $this->assertCount(1, $errors); + $this->assertSame([$file], array_keys($errors)); + + $expected = lang('Publisher.fileNotAllowed', [$file, FCPATH, $pattern]); + $this->assertSame($expected, $errors[$file]->getMessage()); + } + + public function fileProvider() + { + yield 'php' => ['index.php']; + + yield 'exe' => ['cat.exe']; + + yield 'flat' => ['banana']; + } + + /** + * @dataProvider destinationProvider + */ + public function testDestinations(string $destination, bool $allowed) + { + config('Publisher')->restrictions = [ + APPPATH => '', + FCPATH => '', + SUPPORTPATH . 'Files' => '', + SUPPORTPATH . 'Files/../' => '', + ]; + + if (! $allowed) { + $this->expectException(PublisherException::class); + $this->expectExceptionMessage(lang('Publisher.destinationNotAllowed', [$destination])); + } + + $publisher = new Publisher(null, $destination); + $this->assertInstanceOf(Publisher::class, $publisher); + } + + public function destinationProvider() + { + return [ + 'explicit' => [ + APPPATH, + true, + ], + 'subdirectory' => [ + APPPATH . 'Config', + true, + ], + 'relative' => [ + SUPPORTPATH . 'Files/able/../', + true, + ], + 'parent' => [ + SUPPORTPATH, + false, + ], + ]; + } } diff --git a/tests/system/Publisher/PublisherSupportTest.php b/tests/system/Publisher/PublisherSupportTest.php index 081b831f1241..43be04406891 100644 --- a/tests/system/Publisher/PublisherSupportTest.php +++ b/tests/system/Publisher/PublisherSupportTest.php @@ -1,149 +1,155 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + use CodeIgniter\Publisher\Exceptions\PublisherException; use CodeIgniter\Publisher\Publisher; use CodeIgniter\Test\CIUnitTestCase; use Tests\Support\Publishers\TestPublisher; -class PublisherSupportTest extends CIUnitTestCase +/** + * @internal + */ +final class PublisherSupportTest extends CIUnitTestCase { - /** - * A known, valid file - * - * @var string - */ - private $file = SUPPORTPATH . 'Files/baker/banana.php'; - - /** - * A known, valid directory - * - * @var string - */ - private $directory = SUPPORTPATH . 'Files/able/'; - - /** - * Initialize the helper, since some - * tests call static methods before - * the constructor would load it. - */ - public static function setUpBeforeClass(): void - { - parent::setUpBeforeClass(); - - helper(['filesystem']); - } - - //-------------------------------------------------------------------- - - public function testDiscoverDefault() - { - $result = Publisher::discover(); - - $this->assertCount(1, $result); - $this->assertInstanceOf(TestPublisher::class, $result[0]); - } - - public function testDiscoverNothing() - { - $result = Publisher::discover('Nothing'); - - $this->assertSame([], $result); - } - - public function testDiscoverStores() - { - $publisher = Publisher::discover()[0]; - $publisher->set([])->addFile($this->file); - - $result = Publisher::discover(); - $this->assertSame($publisher, $result[0]); - $this->assertSame([$this->file], $result[0]->get()); - } - - //-------------------------------------------------------------------- - - public function testGetSource() - { - $publisher = new Publisher(ROOTPATH); - - $this->assertSame(ROOTPATH, $publisher->getSource()); - } - - public function testGetDestination() - { - $publisher = new Publisher(ROOTPATH, SUPPORTPATH); - - $this->assertSame(SUPPORTPATH, $publisher->getDestination()); - } - - public function testGetScratch() - { - $publisher = new Publisher(); - $this->assertNull($this->getPrivateProperty($publisher, 'scratch')); - - $scratch = $publisher->getScratch(); - - $this->assertIsString($scratch); - $this->assertDirectoryExists($scratch); - $this->assertDirectoryIsWritable($scratch); - $this->assertNotNull($this->getPrivateProperty($publisher, 'scratch')); - - // Directory and contents should be removed on __destruct() - $file = $scratch . 'obvious_statement.txt'; - file_put_contents($file, 'Bananas are a most peculiar fruit'); - - $publisher->__destruct(); - - $this->assertFileDoesNotExist($file); - $this->assertDirectoryDoesNotExist($scratch); - } - - public function testGetErrors() - { - $publisher = new Publisher(); - $this->assertSame([], $publisher->getErrors()); - - $expected = [ - $this->file => PublisherException::forCollision($this->file, $this->file), - ]; - - $this->setPrivateProperty($publisher, 'errors', $expected); - - $this->assertSame($expected, $publisher->getErrors()); - } - - //-------------------------------------------------------------------- - - public function testWipeDirectory() - { - $directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)); - mkdir($directory, 0700); - $this->assertDirectoryExists($directory); - - $method = $this->getPrivateMethodInvoker(Publisher::class, 'wipeDirectory'); - $method($directory); + /** + * A known, valid file + * + * @var string + */ + private $file = SUPPORTPATH . 'Files/baker/banana.php'; + + /** + * A known, valid directory + * + * @var string + */ + private $directory = SUPPORTPATH . 'Files/able/'; + + /** + * Initialize the helper, since some + * tests call static methods before + * the constructor would load it. + */ + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + helper(['filesystem']); + } + + public function testDiscoverDefault() + { + $result = Publisher::discover(); + + $this->assertCount(1, $result); + $this->assertInstanceOf(TestPublisher::class, $result[0]); + } + + public function testDiscoverNothing() + { + $result = Publisher::discover('Nothing'); + + $this->assertSame([], $result); + } + + public function testDiscoverStores() + { + $publisher = Publisher::discover()[0]; + $publisher->set([])->addFile($this->file); + + $result = Publisher::discover(); + $this->assertSame($publisher, $result[0]); + $this->assertSame([$this->file], $result[0]->get()); + } + + public function testGetSource() + { + $publisher = new Publisher(ROOTPATH); + + $this->assertSame(ROOTPATH, $publisher->getSource()); + } + + public function testGetDestination() + { + $publisher = new Publisher(ROOTPATH, SUPPORTPATH); + + $this->assertSame(SUPPORTPATH, $publisher->getDestination()); + } + + public function testGetScratch() + { + $publisher = new Publisher(); + $this->assertNull($this->getPrivateProperty($publisher, 'scratch')); + + $scratch = $publisher->getScratch(); + + $this->assertIsString($scratch); + $this->assertDirectoryExists($scratch); + $this->assertDirectoryIsWritable($scratch); + $this->assertNotNull($this->getPrivateProperty($publisher, 'scratch')); + + // Directory and contents should be removed on __destruct() + $file = $scratch . 'obvious_statement.txt'; + file_put_contents($file, 'Bananas are a most peculiar fruit'); + + $publisher->__destruct(); + + $this->assertFileDoesNotExist($file); + $this->assertDirectoryDoesNotExist($scratch); + } + + public function testGetErrors() + { + $publisher = new Publisher(); + $this->assertSame([], $publisher->getErrors()); + + $expected = [ + $this->file => PublisherException::forCollision($this->file, $this->file), + ]; + + $this->setPrivateProperty($publisher, 'errors', $expected); + + $this->assertSame($expected, $publisher->getErrors()); + } + + public function testWipeDirectory() + { + $directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)); + mkdir($directory, 0700); + $this->assertDirectoryExists($directory); - $this->assertDirectoryDoesNotExist($directory); - } + $method = $this->getPrivateMethodInvoker(Publisher::class, 'wipeDirectory'); + $method($directory); + + $this->assertDirectoryDoesNotExist($directory); + } - public function testWipeIgnoresFiles() - { - $method = $this->getPrivateMethodInvoker(Publisher::class, 'wipeDirectory'); - $method($this->file); + public function testWipeIgnoresFiles() + { + $method = $this->getPrivateMethodInvoker(Publisher::class, 'wipeDirectory'); + $method($this->file); - $this->assertFileExists($this->file); - } + $this->assertFileExists($this->file); + } - public function testWipe() - { - $directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)); - mkdir($directory, 0700); - $this->assertDirectoryExists($directory); - config('Publisher')->restrictions[$directory] = ''; // Allow the directory + public function testWipe() + { + $directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)); + mkdir($directory, 0700); + $this->assertDirectoryExists($directory); + config('Publisher')->restrictions[$directory] = ''; // Allow the directory - $publisher = new Publisher($this->directory, $directory); - $publisher->wipe(); + $publisher = new Publisher($this->directory, $directory); + $publisher->wipe(); - $this->assertDirectoryDoesNotExist($directory); - } + $this->assertDirectoryDoesNotExist($directory); + } } diff --git a/tests/system/Session/Handlers/DatabaseHandlerTest.php b/tests/system/Session/Handlers/DatabaseHandlerTest.php index 7e69bf8e6c73..e001af8eeec8 100644 --- a/tests/system/Session/Handlers/DatabaseHandlerTest.php +++ b/tests/system/Session/Handlers/DatabaseHandlerTest.php @@ -136,6 +136,6 @@ public function testWriteUpdate() public function testGC() { $handler = $this->getInstance(); - $this->assertTrue($handler->gc(3600)); + $this->assertSame(1, $handler->gc(3600)); } } diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 5e3217654e47..1b21fe394659 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -988,11 +988,6 @@ public function validationArrayDataCaseProvider(): iterable /** * @dataProvider provideStringRulesCases * - * @param string $input - * @param array $expected - * - * @return void - * * @see https://github.com/codeigniter4/CodeIgniter4/issues/4929 */ public function testSplittingOfComplexStringRules(string $input, array $expected): void From 692709227bc2e118aec5c669cc0e4db70146cd15 Mon Sep 17 00:00:00 2001 From: Terrorboy Date: Wed, 8 Sep 2021 22:42:47 +0900 Subject: [PATCH 075/490] Fix variable variable `$$id` in RedisHandler --- system/Session/Handlers/RedisHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Session/Handlers/RedisHandler.php b/system/Session/Handlers/RedisHandler.php index 8ed7b8c052b8..ce6e7bde5180 100644 --- a/system/Session/Handlers/RedisHandler.php +++ b/system/Session/Handlers/RedisHandler.php @@ -137,7 +137,7 @@ public function read($id) { if (isset($this->redis) && $this->lockSession($id)) { if (! isset($this->sessionID)) { - $this->sessionID = ${$id}; + $this->sessionID = $id; } $data = $this->redis->get($this->keyPrefix . $id); From 4cd35992e62ef9590f611169cc7fe93902d5eb45 Mon Sep 17 00:00:00 2001 From: MGatner Date: Wed, 8 Sep 2021 14:20:01 +0000 Subject: [PATCH 076/490] Ignore CPD file --- .github/workflows/test-phpcpd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-phpcpd.yml b/.github/workflows/test-phpcpd.yml index fcc66f7d682c..93a27ddf6e28 100644 --- a/.github/workflows/test-phpcpd.yml +++ b/.github/workflows/test-phpcpd.yml @@ -40,4 +40,4 @@ jobs: - name: Detect code duplication run: | sudo phive --no-progress install --global --trust-gpg-keys 4AA394086372C20A phpcpd - phpcpd --exclude system/Test --exclude system/ThirdParty --exclude system/Database/SQLSRV/Builder.php app/ public/ system/ + phpcpd --exclude system/Test --exclude system/ThirdParty --exclude system/Database/SQLSRV/Builder.php system/Database/SQLSRV/Forge.php app/ public/ system/ From ca5537eb2770587e263ac81008fa2d4590e98437 Mon Sep 17 00:00:00 2001 From: MGatner Date: Wed, 8 Sep 2021 14:22:26 +0000 Subject: [PATCH 077/490] Fix typo --- .github/workflows/test-phpcpd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-phpcpd.yml b/.github/workflows/test-phpcpd.yml index 93a27ddf6e28..d78edbea1f0e 100644 --- a/.github/workflows/test-phpcpd.yml +++ b/.github/workflows/test-phpcpd.yml @@ -40,4 +40,4 @@ jobs: - name: Detect code duplication run: | sudo phive --no-progress install --global --trust-gpg-keys 4AA394086372C20A phpcpd - phpcpd --exclude system/Test --exclude system/ThirdParty --exclude system/Database/SQLSRV/Builder.php system/Database/SQLSRV/Forge.php app/ public/ system/ + phpcpd --exclude system/Test --exclude system/ThirdParty --exclude system/Database/SQLSRV/Builder.php --exclude system/Database/SQLSRV/Forge.php app/ public/ system/ From 0c2a8119400bf7f64a2fe53eec9c745f25644c7b Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Wed, 8 Sep 2021 22:58:39 +0800 Subject: [PATCH 078/490] Update 'updated_at' when enabled in replace() --- system/BaseModel.php | 4 +++ system/Database/Postgre/Builder.php | 23 ++++++++----- system/Database/SQLSRV/Builder.php | 4 +-- tests/system/Models/ReplaceModelTest.php | 44 ++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 tests/system/Models/ReplaceModelTest.php diff --git a/system/BaseModel.php b/system/BaseModel.php index e57ac4fec6bd..29c6d1075949 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1031,6 +1031,10 @@ public function replace(?array $data = null, bool $returnSQL = false) return false; } + if ($this->useTimestamps && $this->updatedField && ! array_key_exists($this->updatedField, (array) $data)) { + $data[$this->updatedField] = $this->setDate(); + } + return $this->doReplace($data, $returnSQL); } diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 2b7a5b3dee06..1006ab20012e 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -39,8 +39,6 @@ class Builder extends BaseBuilder ]; /** - * Compile Ignore Statement - * * Checks if the ignore option is supported by * the Database Driver for the specific statement. * @@ -122,13 +120,11 @@ public function decrement(string $column, int $value = 1) * we simply do a DELETE and an INSERT on the first key/value * combo, assuming that it's either the primary key or a unique key. * - * @param array $set An associative array of insert values + * @param array|null $set An associative array of insert values * * @throws DatabaseException * * @return mixed - * - * @internal */ public function replace(?array $set = null) { @@ -145,18 +141,27 @@ public function replace(?array $set = null) } $table = $this->QBFrom[0]; + $set = $this->binds; + + array_walk($set, static function (array &$item) { + $item = $item[0]; + }); $key = array_key_first($set); $value = $set[$key]; $builder = $this->db->table($table); - $exists = $builder->where("{$key} = {$value}", null, false)->get()->getFirstRow(); + $exists = $builder->where($key, $value, true)->get()->getFirstRow(); - if (empty($exists)) { + if (empty($exists) && $this->testMode) { + $result = $this->getCompiledInsert(); + } elseif (empty($exists)) { $result = $builder->insert($set); + } elseif ($this->testMode) { + $result = $this->where($key, $value, true)->getCompiledUpdate(); } else { - array_pop($set); - $result = $builder->update($set, "{$key} = {$value}"); + array_shift($set); + $result = $builder->where($key, $value, true)->update($set); } unset($builder); diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index cda4fac8a825..851f7fd07860 100755 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -348,9 +348,9 @@ protected function _replace(string $table, array $keys, array $values): string $bingo = []; foreach ($common as $v) { - $k = array_search($v, $escKeyFields, true); + $k = array_search($v, $keys, true); - $bingo[$keyFields[$k]] = $binds[trim($values[$k], ':')]; + $bingo[$keys[$k]] = $binds[trim($values[$k], ':')]; } // Querying existing data diff --git a/tests/system/Models/ReplaceModelTest.php b/tests/system/Models/ReplaceModelTest.php new file mode 100644 index 000000000000..cfa5b8af8573 --- /dev/null +++ b/tests/system/Models/ReplaceModelTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Models; + +use Tests\Support\Models\UserModel; + +/** + * @internal + */ +final class ReplaceModelTest extends LiveModelTestCase +{ + public function testReplaceRespectsUseTimestamps(): void + { + $this->createModel(UserModel::class); + + $data = [ + 'name' => 'Amanda Holmes', + 'email' => 'amanda@holmes.com', + 'country' => 'US', + ]; + + $id = $this->model->insert($data); + + $data['id'] = $id; + $data['country'] = 'UK'; + + $sql = $this->model->replace($data, true); + $this->assertStringNotContainsString('updated_at', $sql); + + $this->model = $this->createModel(UserModel::class); + $this->setPrivateProperty($this->model, 'useTimestamps', true); + $sql = $this->model->replace($data, true); + $this->assertStringContainsString('updated_at', $sql); + } +} From e023f02587863201f6e27aa87df082b50469ba72 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Thu, 9 Sep 2021 07:44:47 +0800 Subject: [PATCH 079/490] Document new coding style guide * Document new coding style guide * Update contributing/styleguide.md Co-authored-by: MGatner Co-authored-by: MGatner --- contributing/README.rst | 2 +- contributing/styleguide.md | 259 ++++++++++++++++++++++++++++ contributing/styleguide.rst | 327 ------------------------------------ 3 files changed, 260 insertions(+), 328 deletions(-) create mode 100644 contributing/styleguide.md delete mode 100644 contributing/styleguide.rst diff --git a/contributing/README.rst b/contributing/README.rst index b5539f8a8ae1..0e73b9e5d759 100644 --- a/contributing/README.rst +++ b/contributing/README.rst @@ -8,7 +8,7 @@ Contributing to CodeIgniter - `Contribution CSS <./css.rst>`_ - `Framework internals <./internals.rst>`_ - `CodeIgniter documentation <./documentation.rst>`_ -- `PHP Style Guide <./styleguide.rst>`_ +- `CodeIgniter Coding Style Guide <./styleguide.md>`_ - `Developer's Certificate of Origin <../DCO.txt>`_ CodeIgniter is a community driven project and accepts contributions of code diff --git a/contributing/styleguide.md b/contributing/styleguide.md new file mode 100644 index 000000000000..d746e672d33e --- /dev/null +++ b/contributing/styleguide.md @@ -0,0 +1,259 @@ +# CodeIgniter Coding Style Guide + +This document declares a set of coding conventions and rules to be followed when contributing PHP code +to the CodeIgniter project. + +**Note:** +> While we would recommend it, there's no requirement that you follow these conventions and rules in your +own projects. Usage is discretionary within your projects but strictly enforceable within the framework. + +We follow the [PSR-12: Extended Coding Style][psr12] plus a set of our own +styling conventions. + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", +"MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](http://tools.ietf.org/html/rfc2119). + +_Portions of the following rules are from and attributed to [PSR-12][psr12]. Even if we do not copy all the rules to this coding style guide explicitly, such uncopied rules SHALL still apply._ + +[psr12]: https://www.php-fig.org/psr/psr-12/ +## Implementation + +Our team uses [PHP CS Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer) to apply coding standard fixes automatically. If you would like to leverage these tools yourself visit the [Official CodeIgniter Coding Standard](https://github.com/CodeIgniter/coding-standard) repository for details. + +## General + +### Files + +- All PHP files MUST use the Unix LF (linefeed) line ending only. +- All PHP files MUST end with a non-blank line, terminated with a single LF. +- The closing `?>` tag MUST be omitted from files containing only PHP. + +### Lines + +- There MUST NOT be a hard limit on line length. +- The soft limit on line length MUST be 120 characters. +- Lines SHOULD NOT be longer than 80 characters; lines longer than that SHOULD be split into multiple subsequent lines of no more than 80 characters each. +- There MUST NOT be trailing whitespace at the end of lines. +- Blank lines MAY be added to improve readability and to indicate related blocks of code except where explicitly forbidden. +- There MUST NOT be more than one statement per line. + +### Indenting + +- Code MUST use an indent of 4 spaces for each indent level, and MUST NOT use tabs for indenting. + +### Keywords and Types + +- All PHP reserved [keywords][1] and [types][2] MUST be in lower case. +- Any new types and keywords added to future PHP versions MUST be in lower case. +- Short form of type keywords MUST be used i.e. `bool` instead of `boolean`, `int` instead of `integer` etc. + +[1]: http://php.net/manual/en/reserved.keywords.php +[2]: http://php.net/manual/en/reserved.other-reserved-words.php + +## Declare Statements, Namespace, and Import Statements + +The header of a PHP file may consist of a number of different blocks. If present, each of the blocks below +MUST be separated by a single blank line, and MUST NOT contain a blank line. Each block MUST be in the order +listed below, although blocks that are not relevant may be omitted. + +- Opening ` All the preceding rules are quoted from PSR-12. You may visit its website to view the code block samples. + +## Custom Conventions + +### File Naming + +- Files containing PHP code SHOULD end with a ".php" extension. +- Files containing templates SHOULD end with a ".tpl" extension. +- Files containing classes, interfaces, or traits MUST have their base name exactly matching the name +of the classes they declare. +- Files declaring procedural functions SHOULD be written in snake_case format. + +### Naming of Structural Elements + +- Constants MUST be declared in UPPERCASE_SEPARATED_WITH_UNDERSCORES. +- Class names MUST be declared in PascalCase. +- Method and property names MUST be declared in camelCase. +- Procedural functions MUST be in snake_case. +- Abbreviations/acronyms/initialisms SHOULD be written in their own natural format. + +### Logical Operators + +- The negation operator `!` SHOULD have one space from its argument. +```diff +-!$result ++! $result +``` + +- Use parentheses to clarify potentially confusing logical expressions. + +### PHP Docblocks (PHPDoc) + +- There SHOULD be no useless PHPDoc annotations. +```diff +-/** +- * @param string $data Data +- * @return void +- */ + public function analyse(string $data): void {}; +``` + +### PHPUnit Assertions + +- As much as possible, you SHOULD always use the strict version of assertions. +```diff +-$this->assertEquals(12, (int) $axis); ++$this->assertSame(12, (int) $axis); +``` + +- Use the dedicated assertion instead of using internal types. +```diff +-$this->assertSame(true, is_cli()); ++$this->assertTrue(is_cli()); + +-$this->assertTrue(array_key_exists('foo', $array)); ++$this->assertArrayHasKey('foo', $array); +``` diff --git a/contributing/styleguide.rst b/contributing/styleguide.rst deleted file mode 100644 index a500d663fe11..000000000000 --- a/contributing/styleguide.rst +++ /dev/null @@ -1,327 +0,0 @@ -###################### -PHP Coding Style Guide -###################### - -The following document declares a set of coding convention rules to be -followed when contributing PHP code to the CodeIgniter project. - -Some of these rules, like naming conventions for example, *may* be -incorporated into the framework's logic and therefore be functionally -enforced (which would be separately documented), but while we would -recommend it, there's no requirement that you follow these conventions in -your own applications. - -The `PHP Interop Group `_ has proposed a number of -canonical recommendations for PHP code style. CodeIgniter is not a member of -of PHP-FIG. We commend their efforts to unite the PHP community, -but no not agree with all of their recommendations. - -PSR-2 is PHP-FIG's Coding Style Guide. We do not claim conformance with it, -although there are a lot of similarities. The differences will be pointed out -below. - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", -"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to -be interpreted as described in `RFC 2119 `_. - -*Note: When used below, the term "class" refers to all kinds of classes, -interfaces and traits.* - -***** -Files -***** - -Formatting -========== - -- Files MUST use UTF-8 character set encoding without BOM. -- Files MUST use UNIX line endings (LF: `\n`). -- Files MUST end with a single empty line (i.e. LF: `\n`). - -Structure -========= - -- A single file SHOULD NOT declare more than one class. - Examples where we feel that more than one class in a source file - is appropriate: - - - `system/Debug/CustomExceptions` contains a number of CodeIgniter - exceptions and errors, that we want to use for a consistent - experience across applications. - If we stick with the purist route, then each of the 13+/- custom - exceptions would require an additional file, which would have a - performance impact at times. - - `system/HTTP/Response` provides a RedirectException, used with the - Response class. - - `system/Router/Router` similarly provides a RedirectException, used with - the Router class. - -- Files SHOULD either declare symbols (i.e. classes, functions, constants) - or execute non-declarative logic, but SHOULD NOT do both. - -Naming -====== - -- File names MUST end with a ".php" name extension and MUST NOT have - multiple name extensions. -- Files declaring classes, interfaces or traits MUST have names exactly matching - the classes that they declare (obviously excluding the ".php" name extension). -- Files declaring functions SHOULD be named in *snake_case.php*. - -************************************* -Whitespace, indentation and alignment -************************************* - -- Best practice: indentation SHOULD use only tabs. -- Best practice: alignment SHOULD use only spaces. -- If using tabs for anything, you MUST set the tab spacing to 4. - -This will accommodate the widest range of developer environment options, -while maintaining consistency of code appearance. - -Following the "best practice" above, -the following code block would have a single tab at the beginning of -each line containing braces, and two tabs at the beginning of the -nested statements. No alignment is implied:: - - { - $first = 1; - $second = 2; - $third = 3; - } - -Following the "best practice" above, -the following code block would use spaces to have the assignment -operators line up with each other:: - - { - $first = 1; - $second = 2; - $third = 3; - } - -.. note:: Our indenting and alignment convention differs from PSR-2, which - **only** uses spaces for both indenting and alignment. - -- Unnecessary whitespace characters MUST NOT be present anywhere within a - script. - - That includes trailing whitespace after a line of code, two or - more spaces used when only one is necessary (excluding alignment), as - well as any other whitespace usage that is not functionally required or - explicitly described in this document. - -.. note:: With conforming tab settings, alignment spacing should - be preserved in all development environments. - A pull request that deals only with tabs or spaces for alignment - will not be favorably considered. - -**** -Code -**** - -PHP tags -======== - -- Opening tags MUST only use the `` tags SHOULD NOT be used, unless the intention is to start - direct output. - - - Scripts that don't produce output MUST NOT use the closing `?>` tag. - -Namespaces and classes -====================== - -- Class names and namespaces SHOULD be declared in `UpperCamelCase`, - also called `StudlyCaps`, unless - another form is *functionally* required. - - - Abbreviations in namespaces, class names and method names SHOULD be - written in capital letters (e.g. PHP). - -- Class constants MUST be declared in `CAPITALS_SEPARATED_BY_UNDERSCORES`. -- Class methods, property names and other variables MUST be declared in - `lowerCamelCase()`. -- Class methods and properties MUST have visibility declarations (i.e. - `public`, `private` or `protected`). - -Methods -------- - -To maintain consistency between core classes, class properties MUST -be private or protected, and the following public methods -MUST be used for each such property "x" - -- `getX()` when the method returns returns a property value, or null if not set -- `setX(value)` changes a property value, doesn't return anything, and can - throw exceptions -- `hasX()` returns a boolean to if a property exists -- `newX()` creates an instance of a/the component object and returns it, - and can throw exceptions -- `isX()` returns true/false for boolean properties - -- Methods SHOULD use type hints and return type hints - -Procedural code -=============== - -- Function and variable names SHOULD be declared in `snake_case()` (all - lowercase letters, separated by underscores), unless another form is - *functionally* required. -- Constants MUST be declared in `CAPITALS_SEPARATED_BY_UNDERSCORES`. - -Keywords -======== - -- All keywords MUST be written in lowercase letters. This includes "scalar" - types, but does NOT include core PHP classes such as `stdClass` or - `Exception`. -- Adjacent keywords are separated by a single space character. -- The keywords `require`, `require_once`, `include`, `include_once` MUST - be followed by a single space character and MUST NOT be followed by a - parenthesis anywhere within the declaration. -- The `function` keyword MUST be immediately followed by either an opening - parenthesis or a single space and a function name. -- Other keywords not explicitly mentioned in this section MUST be separated - by a single space character from any printable characters around them and - on the same line. - -Operators -========= - -- The single dot concatenation, incrementing, decrementing, error - suppression operators and references MUST NOT be separated from their - subjects. -- Other operators not explicitly mentioned in this section MUST be - separated by a single space character from any printable characters - around them and on the same line. -- An operator MUST NOT be the last set of printable characters on a line. -- An operator MAY be the first set of printable characters on a line. - -Logical Operators -================= - -- Use the symbol versions (**||** and **&&**) of the logical operators - instead of the word versions (**OR** and **AND**). - - - This is consistent with other programming languages - - It avoids the problem of the assignment operator (**=**) having - higher precedence:: - - $result = true && false; // $result is false, expected - $result = true AND false; // $result is true, evaluated as "($result = true) AND false" - $result = (true AND false); // $result is false - -- The logical negation operator MUST be separated from its argument by a - single space, as in **! $result** instead of **!$result** -- If there is potential confusion with a logical expression, then use - parentheses for clarity, as shown above. - -Control Structures -================== - -- Control structures, such as **if/else** statements, **for/foreach** statements, or - **while/do** statements, MUST use a brace-surrounded block for their body - segments. - - Good control structure examples:: - - if ( $foo ) - { - $bar += $baz; - } - else - { - $baz = 'bar'; - } - - Not-acceptable control structures:: - - if ( $foo ) $bar = $oneThing + $anotherThing + $yetAnotherThing + $evenMore; - - if ( $foo ) $bar += $baz; - else $baz = 'bar'; - -Docblocks -========= - -We use phpDocumentor (phpdoc) to generate the API docs, for all of the source -code inside the `system` folder. - -It wants to see a file summary docblock at the top of a PHP file, -before any PHP statements, and then a docblock before each documentable -component, namely any class/interface/trait, and all public and protected -methods/functions/variables. The docblock for a method or function -is expected to describe the parameters, return value, and any exceptions -thrown. - -Deviations from the above are considered errors by phpdoc. - -An example:: - - group = 'Unknown'; - } - - /** - * Model a fruit ripening over time. - * - * @param array $params - */ - abstract public function ripen(array $params); - } - -Other -===== - -- Argument separators (comma: `,`) MUST NOT be preceded by a whitespace - character and MUST be followed by a space character or a newline - (LF: `\n`). -- Semi-colons (i.e. `;`) MUST NOT be preceded by a whitespace character - and MUST be followed by a newline (LF: `\n`). - -- Opening parentheses SHOULD NOT be followed by a space character. -- Closing parentheses SHOULD NOT be preceded by a space character. - -- Opening square brackets SHOULD NOT be followed by a space character, - unless when using the "short array" declaration syntax. -- Closing square brackets SHOULD NOT be preceded by a space character, - unless when using the "short array" declaration syntax. - -- A curly brace SHOULD be the only printable character on a line, unless: - - - When declaring an anonymous function. - - Inside a "variable variable" (i.e. `${$foo}` or `${'foo'.$bar}`). - - Around a variable in a double-quoted string (i.e. `"Foo {$bar}"`). - -.. note:: Our control structures braces convention differs from PSR-2. - We use "Allman style" notation instead. From 31653be8c2ddd7cdb315d5fc6feac63d25570b1e Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Thu, 9 Sep 2021 07:45:29 +0800 Subject: [PATCH 080/490] Update to phpdocumentor v3.1.1 --- .github/workflows/deploy-apidocs.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index 3b441e36485d..a7117d8f1f6a 100644 --- a/.github/workflows/deploy-apidocs.yml +++ b/.github/workflows/deploy-apidocs.yml @@ -41,12 +41,9 @@ jobs: php-version: '8.0' coverage: none - - name: Download phpDocumentor v3.1 - run: | - cd ./source - curl \ - -L https://github.com/phpDocumentor/phpDocumentor/releases/download/v3.1.0/phpDocumentor.phar \ - -o admin/phpDocumentor.phar + - name: Download latest phpDocumentor + working-directory: source + run: phive install --no-progress --trust-gpg-keys 67F861C3D889C656 --target ./admin phpDocumentor - name: Prepare API repo working-directory: api From a0cbea707550c2bfad30e38520a4bfe1c574c023 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Thu, 9 Sep 2021 09:40:51 +0800 Subject: [PATCH 081/490] Add phive to tools in workflow --- .github/workflows/deploy-apidocs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index a7117d8f1f6a..53b358d27f4b 100644 --- a/.github/workflows/deploy-apidocs.yml +++ b/.github/workflows/deploy-apidocs.yml @@ -39,11 +39,12 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.0' + tools: phive coverage: none - name: Download latest phpDocumentor working-directory: source - run: phive install --no-progress --trust-gpg-keys 67F861C3D889C656 --target ./admin phpDocumentor + run: sudo phive install --no-progress --trust-gpg-keys 67F861C3D889C656 --target ./admin phpDocumentor - name: Prepare API repo working-directory: api From a68a267a0d666b2e39fe6d87b31f1551cb4788fa Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Thu, 9 Sep 2021 09:42:51 +0800 Subject: [PATCH 082/490] Fix interchanged command options --- .github/workflows/deploy-apidocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index 53b358d27f4b..5c4d2a12458f 100644 --- a/.github/workflows/deploy-apidocs.yml +++ b/.github/workflows/deploy-apidocs.yml @@ -44,7 +44,7 @@ jobs: - name: Download latest phpDocumentor working-directory: source - run: sudo phive install --no-progress --trust-gpg-keys 67F861C3D889C656 --target ./admin phpDocumentor + run: sudo phive --no-progress install --trust-gpg-keys 67F861C3D889C656 --target ./admin phpDocumentor - name: Prepare API repo working-directory: api From 27b9f3113585b37000d08b837e98ce578c744f3c Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Thu, 9 Sep 2021 09:50:42 +0800 Subject: [PATCH 083/490] Fix path resolution --- .github/workflows/deploy-apidocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index 5c4d2a12458f..4704b4f31696 100644 --- a/.github/workflows/deploy-apidocs.yml +++ b/.github/workflows/deploy-apidocs.yml @@ -44,7 +44,7 @@ jobs: - name: Download latest phpDocumentor working-directory: source - run: sudo phive --no-progress install --trust-gpg-keys 67F861C3D889C656 --target ./admin phpDocumentor + run: sudo phive --no-progress install --trust-gpg-keys 67F861C3D889C656 --target admin phpDocumentor - name: Prepare API repo working-directory: api From 501d823a0556c7e532facc12a00d9fbbe0037aac Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Thu, 9 Sep 2021 09:53:58 +0800 Subject: [PATCH 084/490] Remove phar extension --- .github/workflows/deploy-apidocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index 4704b4f31696..3e19f09de6ad 100644 --- a/.github/workflows/deploy-apidocs.yml +++ b/.github/workflows/deploy-apidocs.yml @@ -56,7 +56,7 @@ jobs: - name: Build API in source repo working-directory: source run: | - php admin/phpDocumentor.phar run --ansi --verbose + php admin/phpDocumentor run --ansi --verbose cp -R ${GITHUB_WORKSPACE}/source/api/build/* ${GITHUB_WORKSPACE}/api/docs - name: Deploy to API repo From 7a6899c443983afb4b95b919d1f5437caf0589cb Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Thu, 9 Sep 2021 10:13:20 +0800 Subject: [PATCH 085/490] Fix faulty phive installation of phpDocumentor --- .github/workflows/deploy-apidocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index 3e19f09de6ad..4a7d20dd502b 100644 --- a/.github/workflows/deploy-apidocs.yml +++ b/.github/workflows/deploy-apidocs.yml @@ -44,7 +44,7 @@ jobs: - name: Download latest phpDocumentor working-directory: source - run: sudo phive --no-progress install --trust-gpg-keys 67F861C3D889C656 --target admin phpDocumentor + run: sudo phive --no-progress install --global --trust-gpg-keys 67F861C3D889C656 phpDocumentor - name: Prepare API repo working-directory: api @@ -56,7 +56,7 @@ jobs: - name: Build API in source repo working-directory: source run: | - php admin/phpDocumentor run --ansi --verbose + phpDocumentor run --ansi --verbose cp -R ${GITHUB_WORKSPACE}/source/api/build/* ${GITHUB_WORKSPACE}/api/docs - name: Deploy to API repo From 9390c69cd91d9e98b9c3884eaf900091fcfacf1f Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 10 Sep 2021 11:05:27 +0900 Subject: [PATCH 086/490] docs: fix RST format --- user_guide_src/source/installation/upgrade_414.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_414.rst b/user_guide_src/source/installation/upgrade_414.rst index 613823132351..d3e19b0a9954 100644 --- a/user_guide_src/source/installation/upgrade_414.rst +++ b/user_guide_src/source/installation/upgrade_414.rst @@ -8,7 +8,7 @@ This release focuses on code style. All changes (except those noted below) are c **Method Scope** The following methods were changed from "public" to "protected" to match their parent class methods and better align with their uses. -If you relied on any of these methods being public (highly unlikely) adjust your code accordingly:: +If you relied on any of these methods being public (highly unlikely) adjust your code accordingly: * ``CodeIgniter\Database\MySQLi\Connection::execute()`` * ``CodeIgniter\Database\MySQLi\Connection::_fieldData()`` From ee3e1e704aa89b471a06d564f213a65921c0f59e Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 10 Sep 2021 11:06:37 +0900 Subject: [PATCH 087/490] docs: update coding-standard links --- .../source/installation/repositories.rst | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/user_guide_src/source/installation/repositories.rst b/user_guide_src/source/installation/repositories.rst index 18bd3e7c6a18..b585ca8e8f6b 100644 --- a/user_guide_src/source/installation/repositories.rst +++ b/user_guide_src/source/installation/repositories.rst @@ -6,17 +6,15 @@ The CodeIgniter 4 open source project has its own There are several development repositories, of interest to potential contributors: -+------------------+--------------+-----------------------------------------------------------------+ -| Repository | Audience | Description | -+==================+==============+=================================================================+ -| CodeIgniter4 | contributors | Project codebase, including tests & user guide sources | -+------------------+--------------+-----------------------------------------------------------------+ -| translations | developers | System message translations | -+------------------+--------------+-----------------------------------------------------------------+ -| coding-standard | contributors | Coding style conventions & rules | -+------------------+--------------+-----------------------------------------------------------------+ -| | | | -+------------------+--------------+-----------------------------------------------------------------+ ++---------------------------------------------------------------------+--------------+-----------------------------------------------------------------+ +| Repository | Audience | Description | ++=====================================================================+==============+=================================================================+ +| CodeIgniter4 | contributors | Project codebase, including tests & user guide sources | ++---------------------------------------------------------------------+--------------+-----------------------------------------------------------------+ +| translations | developers | System message translations | ++---------------------------------------------------------------------+--------------+-----------------------------------------------------------------+ +| `coding-standard `_ | contributors | Coding style conventions & rules | ++---------------------------------------------------------------------+--------------+-----------------------------------------------------------------+ There are also several deployment repositories, referenced in the installation directions. The deployment repositories are built automatically when a new version is released, and they @@ -32,8 +30,6 @@ are not directly contributed to. +------------------+--------------+-----------------------------------------------------------------+ | userguide | anyone | Pre-built user guide | +------------------+--------------+-----------------------------------------------------------------+ -| | | | -+------------------+--------------+-----------------------------------------------------------------+ In all the above, the latest version of a repository can be downloaded by selecting the "releases" link in the secondary navbar inside @@ -50,7 +46,7 @@ These correspond to the repositories mentioned above: - `codeigniter4/framework `_ - `codeigniter4/appstarter `_ - `codeigniter4/translations `_ -- `codeigniter4/coding-standard `_ +- `codeigniter/coding-standard `_ See the :doc:`Installation ` page for more information. From 831c693bdb7a29cc109c9a17606066563fd5db4f Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 10 Sep 2021 09:07:32 +0700 Subject: [PATCH 088/490] Update to latest laminas-escaper 2.9.0 --- .github/workflows/test-phpunit.yml | 1 - admin/framework/composer.json | 2 +- composer.json | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index c093190233cb..aa7d584991a6 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -124,7 +124,6 @@ jobs: run: | composer update --ansi --no-interaction composer remove --ansi --dev --unused -W rector/rector phpstan/phpstan friendsofphp/php-cs-fixer nexusphp/cs-config codeigniter/coding-standard - php -r 'file_put_contents("vendor/laminas/laminas-zendframework-bridge/src/autoload.php", "");' env: COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }} diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 6e3852d2af79..6c02a4b71eb9 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -11,7 +11,7 @@ "ext-json": "*", "ext-mbstring": "*", "kint-php/kint": "^3.3", - "laminas/laminas-escaper": "^2.8", + "laminas/laminas-escaper": "^2.9", "psr/log": "^1.1" }, "require-dev": { diff --git a/composer.json b/composer.json index 7b9d50b8e620..43768c06c524 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "ext-json": "*", "ext-mbstring": "*", "kint-php/kint": "^3.3", - "laminas/laminas-escaper": "^2.8", + "laminas/laminas-escaper": "^2.9", "psr/log": "^1.1" }, "require-dev": { From de4c6abd162a54833f85c12c7adeaecf99d43076 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 10 Sep 2021 13:56:11 +0900 Subject: [PATCH 089/490] docs: add session table change for upgrade --- .../source/installation/upgrade_420.rst | 17 ++++++++++++++++- .../source/installation/upgrading.rst | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_420.rst b/user_guide_src/source/installation/upgrade_420.rst index a306b4978347..cfbcbdbf1aeb 100644 --- a/user_guide_src/source/installation/upgrade_420.rst +++ b/user_guide_src/source/installation/upgrade_420.rst @@ -1,5 +1,5 @@ ############################# -Upgrading from 4.1.3 to 4.2.0 +Upgrading from 4.1.4 to 4.2.0 ############################# **Changes for set() method in BaseBuilder and Model class** @@ -10,3 +10,18 @@ and modified the ``set()`` method, then you need to change its definition from ``public function set($key, ?string $value = '', ?bool $escape = null)`` to ``public function set($key, $value = '', ?bool $escape = null)``. +**Session DatabaseHandler's database table change** + +The types of the following columns in the session table have been changed for optimization. + +- MySQL + - ``timestamp`` +- PostgreSQL + - ``ip_address`` + - ``timestamp`` + - ``data`` + +Update the definition of the session table. See the :doc:`/libraries/sessions` for the new definition. + +The change was introduced in v4.1.2. But due to `a bug `_, +the DatabaseHandler Driver did not work properly. diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index 64696e691dc7..b6ef428e8b47 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -8,6 +8,7 @@ upgrading from. .. toctree:: :titlesonly: + Upgrading from 4.1.4 to 4.2.0 Upgrading from 4.1.3 to 4.1.4 Upgrading from 4.1.2 to 4.1.3 Upgrading from 4.1.1 to 4.1.2 From 5e6b4a3dffa4023956f69a79e7c98cf5b0297cb0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 10 Sep 2021 16:16:40 +0900 Subject: [PATCH 090/490] docs: update PSR Compliance (PSR-12) --- user_guide_src/source/intro/psr.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/intro/psr.rst b/user_guide_src/source/intro/psr.rst index 92f47fc235f0..a7ab555b7a11 100644 --- a/user_guide_src/source/intro/psr.rst +++ b/user_guide_src/source/intro/psr.rst @@ -10,13 +10,13 @@ status of our compliance with the various accepted, and some draft, proposals. **PSR-1: Basic Coding Standard** This recommendation covers basic class, method, and file-naming standards. Our -`style guide `_ +`style guide `_ meets PSR-1 and adds its own requirements on top of it. -**PSR-2: Coding Style Guide** +**PSR-12: Extended Coding Style** -This PSR was fairly controversial when it first came out. CodeIgniter meets many of the recommendations within, -but does not, and will not, meet all of them. +Our +`style guide `_ follows the recommendation plus a set of our own styling conventions. **PSR-3: Logger Interface** From 152c4050dfde9d697f861563b36135a0539b9796 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Sep 2021 18:54:18 +0000 Subject: [PATCH 091/490] Update rector/rector requirement from 0.11.52 to 0.11.53 (#5071) --- composer.json | 2 +- system/Helpers/number_helper.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 43768c06c524..13454e9d77ec 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "phpstan/phpstan": "^0.12.91", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1", - "rector/rector": "0.11.52", + "rector/rector": "0.11.53", "symplify/package-builder": "^9.3" }, "suggest": { diff --git a/system/Helpers/number_helper.php b/system/Helpers/number_helper.php index 6e1391556c49..d7f49c15f852 100644 --- a/system/Helpers/number_helper.php +++ b/system/Helpers/number_helper.php @@ -26,7 +26,7 @@ function number_to_size($num, int $precision = 1, ?string $locale = null) { // Strip any formatting & ensure numeric input try { - $num = 0 + str_replace(',', '', $num); // @phpstan-ignore-line + $num = 0 + str_replace(',', '', $num); } catch (ErrorException $ee) { return false; } From 151cc63e855ade9585d22b5ab28978373b2f5ba6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 11 Sep 2021 23:07:58 +0900 Subject: [PATCH 092/490] docs: fix view_parser --- .../source/outgoing/view_parser.rst | 568 +++++++++--------- 1 file changed, 284 insertions(+), 284 deletions(-) diff --git a/user_guide_src/source/outgoing/view_parser.rst b/user_guide_src/source/outgoing/view_parser.rst index 37c20c82e11e..96b5440887b5 100644 --- a/user_guide_src/source/outgoing/view_parser.rst +++ b/user_guide_src/source/outgoing/view_parser.rst @@ -12,32 +12,32 @@ It can parse simple variables or variable tag pairs. Pseudo-variable names or control constructs are enclosed in braces, like this:: - - - {blog_title} - - -

{blog_heading}

+ + + {blog_title} + + +

{blog_heading}

- {blog_entries} -
{title}
-

{body}

- {/blog_entries} + {blog_entries} +
{title}
+

{body}

+ {/blog_entries} - - + + These variables are not actual PHP variables, but rather plain text representations that allow you to eliminate PHP from your templates (view files). .. note:: CodeIgniter does **not** require you to use this class since - using pure PHP in your view pages (for instance using the - :doc:`View renderer ` ) - lets them run a little faster. - However, some developers prefer to use some form of template engine if - they work with designers who they feel would find some - confusion working with PHP. + using pure PHP in your view pages (for instance using the + :doc:`View renderer ` ) + lets them run a little faster. + However, some developers prefer to use some form of template engine if + they work with designers who they feel would find some + confusion working with PHP. *************************** Using the View Parser Class @@ -45,12 +45,12 @@ Using the View Parser Class The simplest method to load the parser class is through its service:: - $parser = \Config\Services::parser(); + $parser = \Config\Services::parser(); Alternately, if you are not using the ``Parser`` class as your default renderer, you can instantiate it directly:: - $parser = new \CodeIgniter\View\Parser(); + $parser = new \CodeIgniter\View\Parser(); Then you can use any of the three standard rendering methods that it provides: **render(viewpath, options, save)**, **setVar(name, value, context)** and @@ -87,13 +87,13 @@ Parser templates You can use the ``render()`` method to parse (or render) simple templates, like this:: - $data = [ - 'blog_title' => 'My Blog Title', - 'blog_heading' => 'My Blog Heading', - ]; + $data = [ + 'blog_title' => 'My Blog Title', + 'blog_heading' => 'My Blog Heading', + ]; - echo $parser->setData($data) - ->render('blog_template'); + echo $parser->setData($data) + ->render('blog_template'); View parameters are passed to ``setData()`` as an associative array of data to be replaced in the template. In the above example, the @@ -110,18 +110,18 @@ Several options can be passed to the ``render()`` or ``renderString()`` methods. - ``cache`` - the time in seconds, to save a view's results; ignored for renderString() - ``cache_name`` - the ID used to save/retrieve a cached view result; defaults to the viewpath; - ignored for renderString() + ignored for renderString() - ``saveData`` - true if the view data parameters should be retained for subsequent calls; - default is **false** -- ``cascadeData`` - true if pseudo-variable settings should be passed on to nested - substitutions; default is **true** + default is **false** +- ``cascadeData`` - true if pseudo-variable settings should be passed on to nested + substitutions; default is **true** :: - echo $parser->render('blog_template', [ - 'cache' => HOUR, - 'cache_name' => 'something_unique', - ]); + echo $parser->render('blog_template', [ + 'cache' => HOUR, + 'cache_name' => 'something_unique', + ]); *********************** Substitution Variations @@ -134,12 +134,12 @@ The **simple substitution** performed by the parser is a one-to-one replacement of pseudo-variables where the corresponding data parameter has either a scalar or string value, as in this example:: - $template = '{blog_title}'; - $data = ['blog_title' => 'My ramblings']; + $template = '{blog_title}'; + $data = ['blog_title' => 'My ramblings']; - echo $parser->setData($data)->renderString($template); + echo $parser->setData($data)->renderString($template); - // Result: My ramblings + // Result: My ramblings The ``Parser`` takes substitution a lot further with "variable pairs", used for nested substitutions or looping, and with some advanced @@ -147,9 +147,9 @@ constructs for conditional substitution. When the parser executes, it will generally -- handle any conditional substitutions -- handle any nested/looping substitutions -- handle the remaining single substitutions +- handle any conditional substitutions +- handle any nested/looping substitutions +- handle the remaining single substitutions Loop Substitutions ================== @@ -162,20 +162,20 @@ you would like an entire block of variables to be repeated, with each iteration containing new values? Consider the template example we showed at the top of the page:: - - - {blog_title} - - -

{blog_heading}

+ + + {blog_title} + + +

{blog_heading}

- {blog_entries} -
{title}
-

{body}

- {/blog_entries} + {blog_entries} +
{title}
+

{body}

+ {/blog_entries} - - + + In the above code you'll notice a pair of variables: {blog_entries} data... {/blog_entries}. In a case like this, the entire chunk of data @@ -186,20 +186,20 @@ Parsing variable pairs is done using the identical code shown above to parse single variables, except, you will add a multi-dimensional array corresponding to your variable pair data. Consider this example:: - $data = [ - 'blog_title' => 'My Blog Title', - 'blog_heading' => 'My Blog Heading', - 'blog_entries' => [ - ['title' => 'Title 1', 'body' => 'Body 1'], - ['title' => 'Title 2', 'body' => 'Body 2'], - ['title' => 'Title 3', 'body' => 'Body 3'], - ['title' => 'Title 4', 'body' => 'Body 4'], - ['title' => 'Title 5', 'body' => 'Body 5'], - ], - ]; - - echo $parser->setData($data) - ->render('blog_template'); + $data = [ + 'blog_title' => 'My Blog Title', + 'blog_heading' => 'My Blog Heading', + 'blog_entries' => [ + ['title' => 'Title 1', 'body' => 'Body 1'], + ['title' => 'Title 2', 'body' => 'Body 2'], + ['title' => 'Title 3', 'body' => 'Body 3'], + ['title' => 'Title 4', 'body' => 'Body 4'], + ['title' => 'Title 5', 'body' => 'Body 5'], + ], + ]; + + echo $parser->setData($data) + ->render('blog_template'); The value for the pseudo-variable ``blog_entries`` is a sequential array of associative arrays. The outer level does not have keys associated @@ -209,16 +209,16 @@ If your "pair" data is coming from a database result, which is already a multi-dimensional array, you can simply use the database ``getResultArray()`` method:: - $query = $db->query("SELECT * FROM blog"); + $query = $db->query("SELECT * FROM blog"); - $data = [ - 'blog_title' => 'My Blog Title', - 'blog_heading' => 'My Blog Heading', - 'blog_entries' => $query->getResultArray(), - ]; + $data = [ + 'blog_title' => 'My Blog Title', + 'blog_heading' => 'My Blog Heading', + 'blog_entries' => $query->getResultArray(), + ]; - echo $parser->setData($data) - ->render('blog_template'); + echo $parser->setData($data) + ->render('blog_template'); If the array you are trying to loop over contains objects instead of arrays, the parser will first look for an ``asArray`` method on the object. If it exists, @@ -236,17 +236,17 @@ Nested Substitutions A nested substitution happens when the value for a pseudo-variable is an associative array of values, like a record from a database:: - $data = [ - 'blog_title' => 'My Blog Title', - 'blog_heading' => 'My Blog Heading', - 'blog_entry' => [ - 'title' => 'Title 1', - 'body' => 'Body 1', - ], - ]; + $data = [ + 'blog_title' => 'My Blog Title', + 'blog_heading' => 'My Blog Heading', + 'blog_entry' => [ + 'title' => 'Title 1', + 'body' => 'Body 1', + ], + ]; - echo $parser->setData($data) - ->render('blog_template'); + echo $parser->setData($data) + ->render('blog_template'); The value for the pseudo-variable ``blog_entry`` is an associative array. The key/value pairs defined inside it will be exposed inside @@ -254,13 +254,13 @@ the variable pair loop for that variable. A ``blog_template`` that might work for the above:: -

{blog_title} - {blog_heading}

- {blog_entry} -
-

{title}

-

{body}

-
- {/blog_entry} +

{blog_title} - {blog_heading}

+ {blog_entry} +
+

{title}

+

{body}

+
+ {/blog_entry} If you would like the other pseudo-variables accessible inside the "blog_entry" scope, then make sure that the "cascadeData" option is set to true. @@ -273,13 +273,13 @@ comments in a ``{# #}`` symbols. :: - {# This comment is removed during parsing. #} - {blog_entry} -
-

{title}

-

{body}

-
- {/blog_entry} + {# This comment is removed during parsing. #} + {blog_entry} +
+

{title}

+

{body}

+
+ {/blog_entry} Cascading Data ============== @@ -289,30 +289,30 @@ data pairs into the inner substitution. The following example is not impacted by cascading:: - $template = '{name} lives in {location}{city} on {planet}{/location}.'; + $template = '{name} lives in {location}{city} on {planet}{/location}.'; - $data = [ - 'name' => 'George', - 'location' => ['city' => 'Red City', 'planet' => 'Mars'], - ]; + $data = [ + 'name' => 'George', + 'location' => ['city' => 'Red City', 'planet' => 'Mars'], + ]; - echo $parser->setData($data)->renderString($template); - // Result: George lives in Red City on Mars. + echo $parser->setData($data)->renderString($template); + // Result: George lives in Red City on Mars. This example gives different results, depending on cascading:: - $template = '{location}{name} lives in {city} on {planet}{/location}.'; + $template = '{location}{name} lives in {city} on {planet}{/location}.'; - $data = [ - 'name' => 'George', - 'location' => ['city' => 'Red City', 'planet' => 'Mars'], - ]; + $data = [ + 'name' => 'George', + 'location' => ['city' => 'Red City', 'planet' => 'Mars'], + ]; - echo $parser->setData($data)->renderString($template, ['cascadeData'=>false]); - // Result: {name} lives in Red City on Mars. + echo $parser->setData($data)->renderString($template, ['cascadeData'=>false]); + // Result: {name} lives in Red City on Mars. - echo $parser->setData($data)->renderString($template, ['cascadeData'=>true]); - // Result: George lives in Red City on Mars. + echo $parser->setData($data)->renderString($template, ['cascadeData'=>true]); + // Result: George lives in Red City on Mars. Preventing Parsing ================== @@ -322,9 +322,9 @@ section will stay exactly as it is, with no variable substitution, looping, etc, :: - {noparse} -

Untouched Code

- {/noparse} + {noparse} +

Untouched Code

+ {/noparse} Conditional Logic ================= @@ -332,15 +332,15 @@ Conditional Logic The Parser class supports some basic conditionals to handle ``if``, ``else``, and ``elseif`` syntax. All ``if`` blocks must be closed with an ``endif`` tag:: - {if $role=='admin'} -

Welcome, Admin!

- {endif} + {if $role=='admin'} +

Welcome, Admin!

+ {endif} This simple block is converted to the following during parsing:: - -

Welcome, Admin!

- + +

Welcome, Admin!

+ All variables used within if statements must have been previously set with the same name. Other than that, it is treated exactly like a standard PHP conditional, and all standard PHP rules would apply here. You can use any @@ -348,16 +348,16 @@ of the comparison operators you would normally, like ``==``, ``===``, ``!==``, ` :: - {if $role=='admin'} -

Welcome, Admin

- {elseif $role=='moderator'} -

Welcome, Moderator

- {else} -

Welcome, User

- {endif} + {if $role=='admin'} +

Welcome, Admin

+ {elseif $role=='moderator'} +

Welcome, Moderator

+ {else} +

Welcome, User

+ {endif} .. note:: In the background, conditionals are parsed using an **eval()**, so you must ensure that you take - care with the user data that is used within conditionals, or you could open your application up to security risks. + care with the user data that is used within conditionals, or you could open your application up to security risks. Escaping Data ============= @@ -367,13 +367,13 @@ supports several different contexts, like general **html**, when it's in an HTML else is specified, the data will be assumed to be in an HTML context. You can specify the context used by using the **esc** filter:: - { user_styles | esc(css) } - { title } + { user_styles | esc(css) } + { title } There will be times when you absolutely need something to used and NOT escaped. You can do this by adding exclamation marks to the opening and closing braces:: - {! unescaped_var !} + {! unescaped_var !} Filters ======= @@ -385,17 +385,17 @@ need to format the same data differently in several sections on the same page. Filters are commands that come after the pseudo-variable name, and are separated by the pipe symbol, ``|``:: - // -55 is displayed as 55 - { value|abs } + // -55 is displayed as 55 + { value|abs } If the parameter takes any arguments, they must be separated by commas and enclosed in parentheses:: - { created_at|date(Y-m-d) } + { created_at|date(Y-m-d) } Multiple filters can be applied to the value by piping multiple ones together. They are processed in order, from left to right:: - { created_at|date_modify(+5 days)|date(Y-m-d) } + { created_at|date_modify(+5 days)|date(Y-m-d) } Provided Filters ---------------- @@ -473,10 +473,10 @@ You can easily create your own filters by editing **app/Config/View.php** and ad ``$filters`` array. Each key is the name of the filter is called by in the view, and its value is any valid PHP callable:: - public $filters = [ - 'abs' => '\CodeIgniter\View\Filters::abs', - 'capitalize' => '\CodeIgniter\View\Filters::capitalize', - ]; + public $filters = [ + 'abs' => '\CodeIgniter\View\Filters::abs', + 'capitalize' => '\CodeIgniter\View\Filters::capitalize', + ]; PHP Native functions as Filters ------------------------------- @@ -485,9 +485,9 @@ You can use native php function as filters by editing **app/Config/View.php** an ``$filters`` array.Each key is the name of the native PHP function is called by in the view, and its value is any valid native PHP function prefixed with:: - public $filters = [ - 'str_repeat' => '\str_repeat', - ]; + public $filters = [ + 'str_repeat' => '\str_repeat', + ]; Parser Plugins ============== @@ -495,7 +495,7 @@ Parser Plugins Plugins allow you to extend the parser, adding custom features for each project. They can be any PHP callable, making them very simple to implement. Within templates, plugins are specified by ``{+ +}`` tags:: - {+ foo +} inner content {+ /foo +} + {+ foo +} inner content {+ /foo +} This example shows a plugin named **foo**. It can manipulate any of the content between its opening and closing tags. In this example, it could work with the text " inner content ". Plugins are processed before any pseudo-variable @@ -503,16 +503,16 @@ replacements happen. While plugins will often consist of tag pairs, like shown above, they can also be a single tag, with no closing tag:: - {+ foo +} + {+ foo +} Opening tags can also contain parameters that can customize how the plugin works. The parameters are represented as key/value pairs:: - {+ foo bar=2 baz="x y" } + {+ foo bar=2 baz="x y" } Parameters can also be single values:: - {+ include somefile.php +} + {+ include somefile.php +} Provided Plugins ---------------- @@ -520,14 +520,14 @@ Provided Plugins The following plugins are available when using the parser: ==================== ========================== ================================================================================== ================================================================ -Plugin Arguments Description Example +Plugin Arguments Description Example ==================== ========================== ================================================================================== ================================================================ current_url Alias for the current_url helper function. {+ current_url +} -previous_url Alias for the previous_url helper function. {+ previous_url +} +previous_url Alias for the previous_url helper function. {+ previous_url +} siteURL Alias for the site_url helper function. {+ siteURL "login" +} -mailto email, title, attributes Alias for the mailto helper function. {+ mailto email=foo@example.com title="Stranger Things" +} -safe_mailto email, title, attributes Alias for the safe_mailto helper function. {+ safe_mailto email=foo@example.com title="Stranger Things" +} -lang language string Alias for the lang helper function. {+ lang number.terabyteAbbr +} +mailto email, title, attributes Alias for the mailto helper function. {+ mailto email=foo@example.com title="Stranger Things" +} +safe_mailto email, title, attributes Alias for the safe_mailto helper function. {+ safe_mailto email=foo@example.com title="Stranger Things" +} +lang language string Alias for the lang helper function. {+ lang number.terabyteAbbr +} validation_errors fieldname(optional) Returns either error string for the field (if specified) or all validation errors. {+ validation_errors +} , {+ validation_errors field="email" +} route route name Alias for the route_to helper function. {+ route "login" +} ==================== ========================== ================================================================================== ================================================================ @@ -539,12 +539,12 @@ At its simplest, all you need to do to register a new plugin and make it ready f **app/Config/View.php**, under the **$plugins** array. The key is the name of the plugin that is used within the template file. The value is any valid PHP callable, including static class methods, and closures:: - public $plugins = [ - 'foo' => '\Some\Class::methodName', - 'bar' => function ($str, array $params=[]) { - return $str; - }, - ]; + public $plugins = [ + 'foo' => '\Some\Class::methodName', + 'bar' => function ($str, array $params=[]) { + return $str; + }, + ]; Any closures that are being used must be defined in the config file's constructor:: @@ -565,21 +565,21 @@ Any closures that are being used must be defined in the config file's constructo If the callable is on its own, it is treated as a single tag, not a open/close one. It will be replaced by the return value from the plugin:: - public $plugins = [ - 'foo' => '\Some\Class::methodName' - ]; + public $plugins = [ + 'foo' => '\Some\Class::methodName' + ]; - // Tag is replaced by the return value of Some\Class::methodName static function. - {+ foo +} + // Tag is replaced by the return value of Some\Class::methodName static function. + {+ foo +} If the callable is wrapped in an array, it is treated as an open/close tag pair that can operate on any of the content between its tags:: - public $plugins = [ - 'foo' => ['\Some\Class::methodName'] - ]; + public $plugins = [ + 'foo' => ['\Some\Class::methodName'] + ]; - {+ foo +} inner content {+ /foo +} + {+ foo +} inner content {+ /foo +} *********** Usage Notes @@ -588,49 +588,49 @@ Usage Notes If you include substitution parameters that are not referenced in your template, they are ignored:: - $template = 'Hello, {firstname} {lastname}'; - $data = [ - 'title' => 'Mr', - 'firstname' => 'John', - 'lastname' => 'Doe' - ]; - echo $parser->setData($data) - ->renderString($template); + $template = 'Hello, {firstname} {lastname}'; + $data = [ + 'title' => 'Mr', + 'firstname' => 'John', + 'lastname' => 'Doe' + ]; + echo $parser->setData($data) + ->renderString($template); - // Result: Hello, John Doe + // Result: Hello, John Doe If you do not include a substitution parameter that is referenced in your template, the original pseudo-variable is shown in the result:: - $template = 'Hello, {firstname} {initials} {lastname}'; - $data = [ - 'title' => 'Mr', - 'firstname' => 'John', - 'lastname' => 'Doe', - ]; - echo $parser->setData($data) - ->renderString($template); + $template = 'Hello, {firstname} {initials} {lastname}'; + $data = [ + 'title' => 'Mr', + 'firstname' => 'John', + 'lastname' => 'Doe', + ]; + echo $parser->setData($data) + ->renderString($template); - // Result: Hello, John {initials} Doe + // Result: Hello, John {initials} Doe If you provide a string substitution parameter when an array is expected, i.e., for a variable pair, the substitution is done for the opening variable pair tag, but the closing variable pair tag is not rendered properly:: - $template = 'Hello, {firstname} {lastname} ({degrees}{degree} {/degrees})'; - $data = [ - 'degrees' => 'Mr', - 'firstname' => 'John', - 'lastname' => 'Doe', - 'titles' => [ - ['degree' => 'BSc'], - ['degree' => 'PhD'], - ], - ]; - echo $parser->setData($data) - ->renderString($template); - - // Result: Hello, John Doe (Mr{degree} {/degrees}) + $template = 'Hello, {firstname} {lastname} ({degrees}{degree} {/degrees})'; + $data = [ + 'degrees' => 'Mr', + 'firstname' => 'John', + 'lastname' => 'Doe', + 'titles' => [ + ['degree' => 'BSc'], + ['degree' => 'PhD'], + ], + ]; + echo $parser->setData($data) + ->renderString($template); + + // Result: Hello, John Doe (Mr{degree} {/degrees}) View Fragments ============== @@ -642,53 +642,53 @@ of in the view. An example with the iteration controlled in the view:: - $template = '
    {menuitems} -
  • {title}
  • - {/menuitems}
'; + $template = '
    {menuitems} +
  • {title}
  • + {/menuitems}
'; - $data = [ - 'menuitems' => [ - ['title' => 'First Link', 'link' => '/first'], - ['title' => 'Second Link', 'link' => '/second'], - ] - ]; - echo $parser->setData($data) - ->renderString($template); + $data = [ + 'menuitems' => [ + ['title' => 'First Link', 'link' => '/first'], + ['title' => 'Second Link', 'link' => '/second'], + ] + ]; + echo $parser->setData($data) + ->renderString($template); Result:: - + An example with the iteration controlled in the controller, using a view fragment:: - $temp = ''; - $template1 = '
  • {title}
  • '; - $data1 = [ - ['title' => 'First Link', 'link' => '/first'], - ['title' => 'Second Link', 'link' => '/second'], - ]; + $temp = ''; + $template1 = '
  • {title}
  • '; + $data1 = [ + ['title' => 'First Link', 'link' => '/first'], + ['title' => 'Second Link', 'link' => '/second'], + ]; - foreach ($data1 as $menuItem),{ - $temp .= $parser->setData($menuItem)->renderString($template1); - } + foreach ($data1 as $menuItem),{ + $temp .= $parser->setData($menuItem)->renderString($template1); + } - $template2 = '
      {menuitems}
    '; - $data = [ - 'menuitems' => $temp, - ]; - echo $parser->setData($data) - ->renderString($template2); + $template2 = '
      {menuitems}
    '; + $data = [ + 'menuitems' => $temp, + ]; + echo $parser->setData($data) + ->renderString($template2); Result:: - + *************** Class Reference @@ -696,80 +696,80 @@ Class Reference .. php:class:: CodeIgniter\\View\\Parser - .. php:method:: render($view[, $options[, $saveData=false]]) + .. php:method:: render($view[, $options[, $saveData=false]]) - :param string $view: File name of the view source - :param array $options: Array of options, as key/value pairs - :param boolean $saveData: If true, will save data for use with any other calls, if false, will clean the data after rendering the view. - :returns: The rendered text for the chosen view - :rtype: string + :param string $view: File name of the view source + :param array $options: Array of options, as key/value pairs + :param boolean $saveData: If true, will save data for use with any other calls, if false, will clean the data after rendering the view. + :returns: The rendered text for the chosen view + :rtype: string - Builds the output based upon a file name and any data that has already been set:: + Builds the output based upon a file name and any data that has already been set:: - echo $parser->render('myview'); + echo $parser->render('myview'); Options supported: - - ``cache`` - the time in seconds, to save a view's results - - ``cache_name`` - the ID used to save/retrieve a cached view result; defaults to the viewpath - - ``cascadeData`` - true if the data pairs in effect when a nested or loop substitution occurs should be propagated - - ``saveData`` - true if the view data parameter should be retained for subsequent calls - - ``leftDelimiter`` - the left delimiter to use in pseudo-variable syntax - - ``rightDelimiter`` - the right delimiter to use in pseudo-variable syntax + - ``cache`` - the time in seconds, to save a view's results + - ``cache_name`` - the ID used to save/retrieve a cached view result; defaults to the viewpath + - ``cascadeData`` - true if the data pairs in effect when a nested or loop substitution occurs should be propagated + - ``saveData`` - true if the view data parameter should be retained for subsequent calls + - ``leftDelimiter`` - the left delimiter to use in pseudo-variable syntax + - ``rightDelimiter`` - the right delimiter to use in pseudo-variable syntax - Any conditional substitutions are performed first, then remaining - substitutions are performed for each data pair. + Any conditional substitutions are performed first, then remaining + substitutions are performed for each data pair. - .. php:method:: renderString($template[, $options[, $saveData=false]]) + .. php:method:: renderString($template[, $options[, $saveData=false]]) - :param string $template: View source provided as a string - :param array $options: Array of options, as key/value pairs - :param boolean $saveData: If true, will save data for use with any other calls, if false, will clean the data after rendering the view. - :returns: The rendered text for the chosen view - :rtype: string + :param string $template: View source provided as a string + :param array $options: Array of options, as key/value pairs + :param boolean $saveData: If true, will save data for use with any other calls, if false, will clean the data after rendering the view. + :returns: The rendered text for the chosen view + :rtype: string - Builds the output based upon a provided template source and any data that has already been set:: + Builds the output based upon a provided template source and any data that has already been set:: - echo $parser->render('myview'); + echo $parser->render('myview'); Options supported, and behavior, as above. - .. php:method:: setData([$data[, $context=null]]) + .. php:method:: setData([$data[, $context=null]]) - :param array $data: Array of view data strings, as key/value pairs - :param string $context: The context to use for data escaping. - :returns: The Renderer, for method chaining - :rtype: CodeIgniter\\View\\RendererInterface. + :param array $data: Array of view data strings, as key/value pairs + :param string $context: The context to use for data escaping. + :returns: The Renderer, for method chaining + :rtype: CodeIgniter\\View\\RendererInterface. - Sets several pieces of view data at once:: + Sets several pieces of view data at once:: - $renderer->setData(['name'=>'George', 'position'=>'Boss']); + $renderer->setData(['name'=>'George', 'position'=>'Boss']); Supported escape contexts: html, css, js, url, or attr or raw. - If 'raw', no escaping will happen. + If 'raw', no escaping will happen. - .. php:method:: setVar($name[, $value=null[, $context=null]]) + .. php:method:: setVar($name[, $value=null[, $context=null]]) - :param string $name: Name of the view data variable - :param mixed $value: The value of this view data - :param string $context: The context to use for data escaping. - :returns: The Renderer, for method chaining - :rtype: CodeIgniter\\View\\RendererInterface. + :param string $name: Name of the view data variable + :param mixed $value: The value of this view data + :param string $context: The context to use for data escaping. + :returns: The Renderer, for method chaining + :rtype: CodeIgniter\\View\\RendererInterface. - Sets a single piece of view data:: + Sets a single piece of view data:: - $renderer->setVar('name','Joe','html'); + $renderer->setVar('name','Joe','html'); Supported escape contexts: html, css, js, url, attr or raw. - If 'raw', no escaping will happen. + If 'raw', no escaping will happen. - .. php:method:: setDelimiters($leftDelimiter = '{', $rightDelimiter = '}') + .. php:method:: setDelimiters($leftDelimiter = '{', $rightDelimiter = '}') - :param string $leftDelimiter: Left delimiter for substitution fields - :param string $rightDelimiter: right delimiter for substitution fields - :returns: The Renderer, for method chaining - :rtype: CodeIgniter\\View\\RendererInterface. + :param string $leftDelimiter: Left delimiter for substitution fields + :param string $rightDelimiter: right delimiter for substitution fields + :returns: The Renderer, for method chaining + :rtype: CodeIgniter\\View\\RendererInterface. - Override the substitution field delimiters:: + Override the substitution field delimiters:: - $renderer->setDelimiters('[',']'); + $renderer->setDelimiters('[',']'); From 642924553c84dce8cda11e3cf59a7151aaf573f6 Mon Sep 17 00:00:00 2001 From: Niklas Schmolke Date: Mon, 12 Apr 2021 17:26:37 +0200 Subject: [PATCH 093/490] Added upgrade guidelines --- .../source/installation/upgrade_database.rst | 68 ++++++++ .../source/installation/upgrade_emails.rst | 60 +++++++ .../installation/upgrade_encryption.rst | 51 ++++++ .../installation/upgrade_file_upload.rst | 108 ++++++++++++ .../installation/upgrade_html_tables.rst | 55 ++++++ .../installation/upgrade_localization.rst | 66 ++++++++ .../installation/upgrade_pagination.rst | 62 +++++++ .../source/installation/upgrade_responses.rst | 47 ++++++ .../source/installation/upgrade_routing.rst | 77 +++++++++ .../source/installation/upgrade_security.rst | 71 ++++++++ .../source/installation/upgrade_sessions.rst | 53 ++++++ .../installation/upgrade_validations.rst | 159 ++++++++++++++++++ .../installation/upgrade_view_parser.rst | 56 ++++++ 13 files changed, 933 insertions(+) create mode 100644 user_guide_src/source/installation/upgrade_database.rst create mode 100644 user_guide_src/source/installation/upgrade_emails.rst create mode 100644 user_guide_src/source/installation/upgrade_encryption.rst create mode 100644 user_guide_src/source/installation/upgrade_file_upload.rst create mode 100644 user_guide_src/source/installation/upgrade_html_tables.rst create mode 100644 user_guide_src/source/installation/upgrade_localization.rst create mode 100644 user_guide_src/source/installation/upgrade_pagination.rst create mode 100644 user_guide_src/source/installation/upgrade_responses.rst create mode 100644 user_guide_src/source/installation/upgrade_routing.rst create mode 100644 user_guide_src/source/installation/upgrade_security.rst create mode 100644 user_guide_src/source/installation/upgrade_sessions.rst create mode 100644 user_guide_src/source/installation/upgrade_validations.rst create mode 100644 user_guide_src/source/installation/upgrade_view_parser.rst diff --git a/user_guide_src/source/installation/upgrade_database.rst b/user_guide_src/source/installation/upgrade_database.rst new file mode 100644 index 000000000000..2c6a05e987f5 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_database.rst @@ -0,0 +1,68 @@ +Upgrade Database +################ + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== + +- `Database Reference Documentation Codeigniter 3.X `_ +- `Working with Databases Documentation Codeigniter 4.X `_ + + +What has been changed +===================== +- The functionality in CI4 is basically the same as in CI4. +- The method names have changed to camelCase and the query builder now needs to be initialized before you can run queries on it. + +Upgrade Guide +============= +1. Add your database credentials to ``app/Config/Database.php``. The options are pretty much the same as in CI3 only some names have changed slightly. +2. Everywhere you have used the database you have to replace ``$this->load->database();`` with ``$db = \Config\Database::connect();``. +3. If you use multiple databases use the following code to load additional databases ``$db = \Config\Database::connect('group_name');``. +4. Now you have to change all database queries. The most important change here is to replace ``$this->db`` with just ``$db`` and adjust the method name and ``$db``. Here are some examples: + +- ``$this->db->query('YOUR QUERY HERE');`` to ``$db->query('YOUR QUERY HERE');`` +- ``$this->db->simple_query('YOUR QUERY')`` to ``$db->simpleQuery('YOUR QUERY')`` +- ``$this->db->escape("something")`` to ``$db->escape("something");`` +- ``$this->db->affected_rows();`` to ``$db->affectedRows();`` +- ``$query->result();`` to ``$query->getResult();`` +- ``$query->result_array();`` to ``$query->getResultArray();`` +- ``echo $this->db->count_all('my_table');`` to ``echo $db->table('my_table')->countAll();`` + +5. To use the new Query Builder Class you have to initialize the builder ``$builder = $db->table('mytable');`` after that you can run the queries on the ``$builder``. Here are some examples: + +- ``$this->db->get()`` to ``$builder->get();`` +- ``$this->db->get_where('mytable', array('id' => $id), $limit, $offset);`` to ``$builder->getWhere(['id' => $id], $limit, $offset);`` +- ``$this->db->select('title, content, date');`` to ``$builder->select('title, content, date');`` +- ``$this->db->select_max('age');`` to ``$builder->selectMax('age');`` +- ``$this->db->join('comments', 'comments.id = blogs.id');`` to ``$builder->join('comments', 'comments.id = blogs.id');`` +- ``$this->db->having('user_id', 45);`` to ``$builder->having('user_id', 45);`` + + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +:: + + $query = $this->db->select('title') + ->where('id', $id) + ->limit(10, 20) + ->get('mytable'); + +Codeigniter Version 4.x +----------------------- +:: + + $builder = $db->table('mytable'); + + $query = $builder->select('title') + ->where('id', $id) + ->limit(10, 20) + ->get(); + diff --git a/user_guide_src/source/installation/upgrade_emails.rst b/user_guide_src/source/installation/upgrade_emails.rst new file mode 100644 index 000000000000..b0007289020e --- /dev/null +++ b/user_guide_src/source/installation/upgrade_emails.rst @@ -0,0 +1,60 @@ +Upgrade Emails +############## + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== + +- `Email Documentation Codeigniter 3.X `_ +- `Email Documentation Codeigniter 4.X `_ + + +What has been changed +===================== +- Only small things like the method names and the loading of the library have changed. + +Upgrade Guide +============= +1. Within your class change the ``$this->load->library('email');`` to ``$email = \Config\Services::email();``. +2. From that on you have to replace every line starting with ``$this->email`` to ``$email``. +3. The methods in the Email class are named slightly different. All methods, except for ``send()``, ``attach()``, ``printDebugger()`` and ``clear()`` have a ``set`` as prefix followed by the previous method name. ``bcc()`` is now ``setBcc()`` and so on. +4. The config attributes in ``app/Config/Email.php`` have changed. You should have a look at the `Email Class Documentation `_ to have a list of the new attributes. + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +:: + + $this->load->library('email'); + + $this->email->from('your@example.com', 'Your Name'); + $this->email->to('someone@example.com'); + $this->email->cc('another@another-example.com'); + $this->email->bcc('them@their-example.com'); + + $this->email->subject('Email Test'); + $this->email->message('Testing the email class.'); + + $this->email->send(); + +Codeigniter Version 4.x +----------------------- +:: + + $email = \Config\Services::email(); + + $email->setFrom('your@example.com', 'Your Name'); + $email->setTo('someone@example.com'); + $email->setCC('another@another-example.com'); + $email->setBCC('them@their-example.com'); + + $email->setSubject('Email Test'); + $email->setMessage('Testing the email class.'); + + $email->send(); diff --git a/user_guide_src/source/installation/upgrade_encryption.rst b/user_guide_src/source/installation/upgrade_encryption.rst new file mode 100644 index 000000000000..ee1365cd5c42 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_encryption.rst @@ -0,0 +1,51 @@ +Upgrade Encryption +################## + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== + +- `Encryption Library Documentation Codeigniter 3.X `_ +- `Encryption Service Documentation Codeigniter 4.X `_ + + +What has been changed +===================== +- The support for ``MCrypt`` has been dropped, as that has been deprecated as of PHP 7.2. + +Upgrade Guide +============= +1. Within your configs the ``$config['encryption_key'] = 'abc123';`` moved from ``application/config/config.php`` to ``public $key = 'abc123';`` in ``app/Config/Encryption.php``. +2. Wherever you have used the encryption library you have to replace ``$this->load->library('encryption');`` with ``$encrypter = \Config\Services::encrypter();`` and change the methods for encryption and decrypting like in the following code example. + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +:: + + $this->load->library('encryption'); + + $plain_text = 'This is a plain-text message!'; + $ciphertext = $this->encryption->encrypt($plain_text); + + // Outputs: This is a plain-text message! + echo $this->encryption->decrypt($ciphertext); + + +Codeigniter Version 4.x +----------------------- +:: + + $encrypter = \Config\Services::encrypter(); + + $plainText = 'This is a plain-text message!'; + $ciphertext = $encrypter->encrypt($plainText); + + // Outputs: This is a plain-text message! + echo $encrypter->decrypt($ciphertext); diff --git a/user_guide_src/source/installation/upgrade_file_upload.rst b/user_guide_src/source/installation/upgrade_file_upload.rst new file mode 100644 index 000000000000..069164dd2f68 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_file_upload.rst @@ -0,0 +1,108 @@ +Upgrade Working with Uploaded Files +################################### + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== +- `Output Class Documentation Codeigniter 3.X `_ +- `Working with Uploaded Files Documentation Codeigniter 4.X `_ + +What has been changed +===================== +- The functionality of the file upload has changed a lot. You can now check if the file got uploaded without errors and moving / storing files is simpler. + +Upgrade Guide +============= +In CI4 you access uploaded files with ``$file = $this->request->getFile('userfile')``. From there you can validate if the file got uploaded successfully with ``$file->isValid()``. +To store the file you could use ``$path = $this->request->getFile('userfile')->store('head_img/', 'user_name.jpg');`` This will store the file in ``writable/uploads/head_img/user_name.jpg``. + +You have to change your file uploading code to match the new methods. + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +:: + + load->helper(array('form', 'url')); + } + + public function index() + { + $this->load->view('upload_form', array('error' => ' ' )); + } + + public function do_upload() + { + $config['upload_path'] = './uploads/'; + $config['allowed_types'] = 'gif|jpg|png'; + $config['max_size'] = 100; + $config['max_width'] = 1024; + $config['max_height'] = 768; + + $this->load->library('upload', $config); + + if ( ! $this->upload->do_upload('userfile')) + { + $error = array('error' => $this->upload->display_errors()); + + $this->load->view('upload_form', $error); + } + else + { + $data = array('upload_data' => $this->upload->data()); + + $this->load->view('upload_success', $data); + } + } + } + ?> + +Codeigniter Version 4.x +----------------------- +:: + + ' ']); + } + + public function do_upload() + { + $this->validate([ + 'userfile' => 'uploaded[userfile]|max_size[userfile,1024]|mime_in[userfile,image/png,image/jpg,image/gif]|max_dims[userfile,1024,768]' + ]); + + $file = $this->request->getFile('userfile'); + + if ( ! $path = $file->store()) + { + echo view('upload_form', ['error' => "upload failed"]); + } + else + { + $data = ['upload_file_path' => $path]; + + echo view('upload_success', $data); + } + } + } + ?> diff --git a/user_guide_src/source/installation/upgrade_html_tables.rst b/user_guide_src/source/installation/upgrade_html_tables.rst new file mode 100644 index 000000000000..82d75f143584 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_html_tables.rst @@ -0,0 +1,55 @@ +Upgrade HTML Tables +################### + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== + +- `HTML Table Documentation Codeigniter 3.X `_ +- `HTML Table Documentation Codeigniter 4.X `_ + + +What has been changed +===================== +- Only small things like the method names and the loading of the library have changed. + +Upgrade Guide +============= +1. Within your class change the ``$this->load->library('table');`` to ``$table = new \CodeIgniter\View\Table();``. +2. From that on you have to replace every line starting with ``$this->table`` to ``$table``. For example: ``echo $this->table->generate($query);`` will become ``echo $table->generate($query);`` +3. The methods in the HTML Table class could be named slightly different. The most important change in the naming is the switch from underscored method names to camelCase. The method ``set_heading()`` from version 3 is now named ``setHeading()`` and so on. + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +:: + + $this->load->library('table'); + + $this->table->set_heading('Name', 'Color', 'Size'); + + $this->table->add_row('Fred', 'Blue', 'Small'); + $this->table->add_row('Mary', 'Red', 'Large'); + $this->table->add_row('John', 'Green', 'Medium'); + + echo $this->table->generate(); + +Codeigniter Version 4.x +----------------------- +:: + + $table = new \CodeIgniter\View\Table(); + + $table->setHeading('Name', 'Color', 'Size'); + + $table->addRow('Fred', 'Blue', 'Small'); + $table->addRow('Mary', 'Red', 'Large'); + $table->addRow('John', 'Green', 'Medium'); + + echo $table->generate(); diff --git a/user_guide_src/source/installation/upgrade_localization.rst b/user_guide_src/source/installation/upgrade_localization.rst new file mode 100644 index 000000000000..14c6f7099f44 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_localization.rst @@ -0,0 +1,66 @@ +Upgrade Localization +#################### + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== + +- `Language Documentation Codeigniter 3.X `_ +- `Localization Documentation Codeigniter 4.X `_ + + +What has been changed +===================== +- In CI4 the language files return the language lines as array. + +Upgrade Guide +============= +1. Specify the default language in *Config/App.php*::: + + public $defaultLocale = 'en'; + +2. Now move your language files to ``app/Language//``. +3. After that you have to change the syntax within the language files. Below in the Code Example you will see how the language array within the file should look like. +4. Remove from every file the language loader ``$this->lang->load($file, $lang);``. +5. Replace the method to load the language line ``$this->lang->line('error_email_missing')`` with ``echo lang('Errors.errorEmailMissing');``. + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +:: + + $lang['error_email_missing'] = 'You must submit an email address'; + $lang['error_url_missing'] = 'You must submit a URL'; + $lang['error_username_missing'] = 'You must submit a username'; + + ... + + $this->lang->load('error', $lang); + echo $this->lang->line('error_email_missing'); + +Codeigniter Version 4.x +----------------------- +:: + + return [ + 'errorEmailMissing' => 'You must submit an email address', + 'errorURLMissing' => 'You must submit a URL', + 'errorUsernameMissing' => 'You must submit a username', + 'nested' => [ + 'error' => [ + 'message' => 'A specific error message', + ], + ], + ]; + + ... + + echo lang('Errors.errorEmailMissing'); + + diff --git a/user_guide_src/source/installation/upgrade_pagination.rst b/user_guide_src/source/installation/upgrade_pagination.rst new file mode 100644 index 000000000000..bb5ffedbc632 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_pagination.rst @@ -0,0 +1,62 @@ +Upgrade Pagination +################## + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== + +- `Pagination Class Documentation Codeigniter 3.X `_ +- `Pagination Documentation Codeigniter 4.X `_ + + +What has been changed +===================== +- In CI4 the pagination is a built-in method in the Model class. You have to change the views and also the controller in order to use the new pagination library. + +Upgrade Guide +============= +1. Within the views change to following: + +- ``pagination->create_links(); ?>`` to ``links() ?>`` + +2. Within the controller you have to make the following changes: + +- You can use the built-in ``paginate()`` method on every Model. Have a look at the code example below to see how you setup the pagination on a specific model. + + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +:: + + $this->load->library('pagination'); + $config['base_url'] = base_url().'users/index/'; + $config['total_rows'] = $this->db->count_all('users'); + $config['per_page'] = 10; + $config['uri_segment'] = 3; + $config['attributes'] = array('class' => 'pagination-link'); + $this->pagination->initialize($config); + + $data['users'] = $this->user_model->get_users(FALSE, $config['per_page'], $offset); + + $this->load->view('posts/index', $data); + +Codeigniter Version 4.x +----------------------- +:: + + $model = new \App\Models\UserModel(); + + $data = [ + 'users' => $model->paginate(10), + 'pager' => $model->pager, + ]; + + echo view('users/index', $data); + diff --git a/user_guide_src/source/installation/upgrade_responses.rst b/user_guide_src/source/installation/upgrade_responses.rst new file mode 100644 index 000000000000..c0ba918cbb68 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_responses.rst @@ -0,0 +1,47 @@ +Upgrade HTTP Responses +###################### + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== +- `Output Class Documentation Codeigniter 3.X `_ +- `HTTP Responses Documentation Codeigniter 4.X `_ + +What has been changed +===================== +- The methods have been renamed + +Upgrade Guide +============= +1. The methods in the HTML Responses class are named slightly different. The most important change in the naming is the switch from underscored method names to camelCase. The method ``set_content_type()`` from version 3 is now named ``setContentType()`` and so on. +2. In the most cases you have to change ``$this->output`` to ``$this->response`` followed by the method. You can find all methods `here `_. + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +:: + + $this->output->set_status_header(404); + + ... + + $this->output + ->set_content_type('application/json') + ->set_output(json_encode(array('foo' => 'bar'))); + +Codeigniter Version 4.x +----------------------- +:: + + $this->response->setStatusCode(404) + ->setBody($body); + + ... + + return $this->response->setJSON(['foo' => 'bar']); diff --git a/user_guide_src/source/installation/upgrade_routing.rst b/user_guide_src/source/installation/upgrade_routing.rst new file mode 100644 index 000000000000..575a0699561d --- /dev/null +++ b/user_guide_src/source/installation/upgrade_routing.rst @@ -0,0 +1,77 @@ +Upgrade Routing +################## + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== + +- `URI Routing Documentation Codeigniter 3.X `_ +- `URI Routing Documentation Codeigniter 4.X `_ + + +What has been changed +===================== +- In CI4 the routing is no longer configured by setting the routes as array. + +Upgrade Guide +============= +1. You have to change the syntax of each routing line and append it in ``app/Config/Routes.php``. For example: + +- ``$route['journals'] = 'blogs';`` to ``$routes->add('journals', 'App\Blogs');`` this would map to the ``index()`` method in the "Blogs" class. +- ``$route['product/(:any)'] = 'catalog/product_lookup';`` to ``$routes->add('product/(:any)', 'Catalog::productLookup');`` +- ``$route['login/(.+)'] = 'auth/login/$1';`` to ``$routes->add('login/(.+)', 'Auth::login/$1');`` + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +Path: ``application/config/routes.php``:: + + add('posts', 'posts::index'); + $routes->add('teams/create', 'teams::create'); + $routes->add('teams/edit/(:any)', 'teams::edit/$1'); + + $routes->add('posts/create', 'posts::create'); + $routes->add('posts/edit/(:any)', 'posts::edit/$1'); + $routes->add('drivers/create', 'drivers::create'); + $routes->add('drivers/edit/(:any)', 'drivers::edit/$1'); + $routes->add('posts/(:any)', 'posts::view/$1'); + diff --git a/user_guide_src/source/installation/upgrade_security.rst b/user_guide_src/source/installation/upgrade_security.rst new file mode 100644 index 000000000000..88f4814ddb12 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_security.rst @@ -0,0 +1,71 @@ +Upgrade Security +################ + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== + +- `Security Class Documentation Codeigniter 3.X `_ +- `Security Documentation Codeigniter 4.X `_ + +.. note:: + If you use the `form helper `_, then form_open() will automatically insert a hidden csrf field in your forms. So you do not have to upgrade this by yourself. + +What has been changed +===================== +- The method to implement csrf tokens to html forms has been changed. + +Upgrade Guide +============= +1. To enable csrf protection in CI4 you have to enable it in ``app/Config/Filters.php``:: + + public $globals = [ + 'before' => [ + //'honeypot' + 'csrf' + ] + ]; + +2. Within you html forms you have to remove the csrf input which looks similar to ````. +3. Now within your html forms you have to add ```` somewhere in the form body, unless you are using `form_open()``. + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +:: + + $csrf = array( + 'name' => $this->security->get_csrf_token_name(), + 'hash' => $this->security->get_csrf_hash() + ); + + ... + +
    + + + + + + +
    + +Codeigniter Version 4.x +----------------------- +:: + +
    + + + + + + +
    + diff --git a/user_guide_src/source/installation/upgrade_sessions.rst b/user_guide_src/source/installation/upgrade_sessions.rst new file mode 100644 index 000000000000..e3d7b4da343b --- /dev/null +++ b/user_guide_src/source/installation/upgrade_sessions.rst @@ -0,0 +1,53 @@ +Upgrade Sessions +################ + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== + +- `Session Library Documentation Codeigniter 3.X `_ +- `Session Library Documentation Codeigniter 4.X `_ + + +What has been changed +===================== +- Only small things like the method names and the loading of the library have changed. + +Upgrade Guide +============= +1. Wherever you use the Session Library replace ``$this->load->library('session');`` with ``$session = \Config\Services::session();``. +2. From that on you have to replace every line starting with ``$this->session`` with ``$session`` followed by the new method name. + +- To access session data use the syntax ``$session->item`` or ``$session->get('item')`` instead of the CI3 syntax ``$this->session->name``. +- To set data use ``$session->set($array);`` instead of ``$this->session->set_userdata($array);``. +- To remove data use ``unset($_SESSION['some_name']);`` or ``$session->remove('some_name');`` instead of ``$this->session->unset_userdata('some_name');``. +- To mark session data as flasdata, which will only be available for the next request, use ``$session->markAsFlashdata('item');`` instead of ``$this->session->mark_as_flash('item');``` + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +:: + + $this->load->library('session'); + + $_SESSION['item']; + $this->session->item; + $this->session->userdata('item'); + +Codeigniter Version 4.x +----------------------- +:: + + $session = \Config\Services::session(); + + $_SESSION['item']; + $session->get('item'); + $session->item; + session('item'); + diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst new file mode 100644 index 000000000000..020952a1d9c6 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -0,0 +1,159 @@ +Upgrade Validations +################### + +.. contents:: + :local: + :depth: 1 + + +Documentations of Library +========================= + +- `Form Validation Documentation Codeigniter 3.X `_ +- `Validation Documentation Codeigniter 4.X `_ + + +What has been changed +===================== + + +Upgrade Guide +============= +1. Within the view which contains the form you have to change: + +- ```` to ``listErrors() ?>`` +- ```` to ```` + +2. Within the controller you have to change the following: + +- ``$this->load->helper(array('form', 'url'));`` to ``helper(['form', 'url']);`` +- remove the line ``$this->load->library('form_validation');`` +- ``if($this->form_validation->run() == FALSE)`` to ``if (! $this->validate([]))`` +- ``$this->load->view('myform');`` to ``echo view('myform', ['validation' => $this->validator,]);`` + +3. You have to change the validation rules. The new syntax is to set the rules as array in the controller:: + + $input = $this->validate([ + 'name' => 'required|min_length[3]', + 'email' => 'required|valid_email', + 'phone' => 'required|numeric|max_length[10]' + ]); + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +Path: ``application/views``:: + + + + My Form + + + + + + + +
    Username
    + + +
    Password
    + + +
    Password Confirm
    + + +
    Email Address
    + + +
    + + + + + + +Path: ``application/controllers/``:: + + load->helper(array('form', 'url')); + + $this->load->library('form_validation'); + + if ($this->form_validation->run() == FALSE) + { + $this->load->view('myform'); + } + else + { + $this->load->view('formsuccess'); + } + } + } + +Codeigniter Version 4.x +----------------------- +Path: ``app/Views``:: + + + + My Form + + + + listErrors() ?> + + + +
    Username
    + + +
    Password
    + + +
    Password Confirm
    + + +
    Email Address
    + + +
    + + + + + + +Path: ``app/Controllers/``:: + + validate([])) + { + echo view('Signup', [ + 'validation' => $this->validator, + ]); + } + else + { + echo view('Success'); + } + } + } \ No newline at end of file diff --git a/user_guide_src/source/installation/upgrade_view_parser.rst b/user_guide_src/source/installation/upgrade_view_parser.rst new file mode 100644 index 000000000000..3b23a4ea1d0b --- /dev/null +++ b/user_guide_src/source/installation/upgrade_view_parser.rst @@ -0,0 +1,56 @@ +Upgrade View Parser +################### + +.. contents:: + :local: + :depth: 1 + + +Documentations +============== + +- `Template Parser Documentation Codeigniter 3.X `_ +- `View Parser Documentation Codeigniter 4.X `_ + + +What has been changed +===================== +- You have to change the implementation and loading of the Parser Library. +- The Views can copied from CI3. Usually no changes there are required. + +Upgrade Guide +============= +1. Wherever you use the View Parser Library replace ``$this->load->library('parser');`` with ``$parser = \Config\Services::parser();``. +2. You have to change the render part in your controller from ``$this->parser->parse('blog_template', $data);`` to ``echo $parser->setData($data)->render('blog_template');``. + +Code Example +============ + +Codeigniter Version 3.11 +------------------------ +:: + + $this->load->library('parser'); + + $data = array( + 'blog_title' => 'My Blog Title', + 'blog_heading' => 'My Blog Heading' + ); + + $this->parser + ->parse('blog_template', $data); + +Codeigniter Version 4.x +----------------------- +:: + + $parser = \Config\Services::parser(); + + $data = [ + 'blog_title' => 'My Blog Title', + 'blog_heading' => 'My Blog Heading' + ]; + + echo $parser->setData($data) + ->render('blog_template'); + From 446974f285e0dec3f221b80a34518d8463abb5ed Mon Sep 17 00:00:00 2001 From: Niklas Schmolke Date: Mon, 12 Apr 2021 17:27:11 +0200 Subject: [PATCH 094/490] Updated upgrade_migrations.rst Signed-off-by: Niklas Schmolke --- .../installation/upgrade_migrations.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/user_guide_src/source/installation/upgrade_migrations.rst b/user_guide_src/source/installation/upgrade_migrations.rst index 2124a67d60b0..f21243a2b565 100644 --- a/user_guide_src/source/installation/upgrade_migrations.rst +++ b/user_guide_src/source/installation/upgrade_migrations.rst @@ -119,3 +119,22 @@ Path: ``app/Database/Migrations``:: $this->forge->dropTable('blog'); } } + +Search & Replace +================ + +You can use to following table to search & replace your old CI3 files. + ++------------------------------+----------------------------+ +| Search | Replace | ++==============================+============================+ +| extends CI_Migration | extends Migration | ++------------------------------+----------------------------+ +| $this->dbforge->add_field | $this->forge->addField | ++------------------------------+----------------------------+ +| $this->dbforge->add_key | $this->forge->addKey | ++------------------------------+----------------------------+ +| $this->dbforge->create_table | $this->forge->createTable | ++------------------------------+----------------------------+ +| $this->dbforge->drop_table | $this->forge->dropTable | ++------------------------------+----------------------------+ From c9d969443ce2ea97d17b0d408818ad484bdebbb6 Mon Sep 17 00:00:00 2001 From: nschmolke <79579458+nschmolke@users.noreply.github.com> Date: Wed, 14 Apr 2021 14:42:21 +0200 Subject: [PATCH 095/490] Apply suggestions from code review Co-authored-by: Toto --- user_guide_src/source/installation/upgrade_database.rst | 5 ++--- user_guide_src/source/installation/upgrade_emails.rst | 4 ++-- user_guide_src/source/installation/upgrade_encryption.rst | 2 +- user_guide_src/source/installation/upgrade_file_upload.rst | 2 +- user_guide_src/source/installation/upgrade_html_tables.rst | 2 +- user_guide_src/source/installation/upgrade_localization.rst | 3 +-- user_guide_src/source/installation/upgrade_pagination.rst | 3 +-- user_guide_src/source/installation/upgrade_responses.rst | 4 ++-- user_guide_src/source/installation/upgrade_routing.rst | 3 +-- user_guide_src/source/installation/upgrade_security.rst | 3 +-- user_guide_src/source/installation/upgrade_sessions.rst | 3 +-- user_guide_src/source/installation/upgrade_validations.rst | 4 ++-- user_guide_src/source/installation/upgrade_view_parser.rst | 3 +-- 13 files changed, 17 insertions(+), 24 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_database.rst b/user_guide_src/source/installation/upgrade_database.rst index 2c6a05e987f5..3472aef4d69e 100644 --- a/user_guide_src/source/installation/upgrade_database.rst +++ b/user_guide_src/source/installation/upgrade_database.rst @@ -10,12 +10,12 @@ Documentations ============== - `Database Reference Documentation Codeigniter 3.X `_ -- `Working with Databases Documentation Codeigniter 4.X `_ +- :doc:`Working with Databases Documentation Codeigniter 4.X ` What has been changed ===================== -- The functionality in CI4 is basically the same as in CI4. +- The functionality in CI3 is basically the same as in CI4. - The method names have changed to camelCase and the query builder now needs to be initialized before you can run queries on it. Upgrade Guide @@ -65,4 +65,3 @@ Codeigniter Version 4.x ->where('id', $id) ->limit(10, 20) ->get(); - diff --git a/user_guide_src/source/installation/upgrade_emails.rst b/user_guide_src/source/installation/upgrade_emails.rst index b0007289020e..0915948afabd 100644 --- a/user_guide_src/source/installation/upgrade_emails.rst +++ b/user_guide_src/source/installation/upgrade_emails.rst @@ -10,7 +10,7 @@ Documentations ============== - `Email Documentation Codeigniter 3.X `_ -- `Email Documentation Codeigniter 4.X `_ +- :doc:`Email Documentation Codeigniter 4.X ` What has been changed @@ -22,7 +22,7 @@ Upgrade Guide 1. Within your class change the ``$this->load->library('email');`` to ``$email = \Config\Services::email();``. 2. From that on you have to replace every line starting with ``$this->email`` to ``$email``. 3. The methods in the Email class are named slightly different. All methods, except for ``send()``, ``attach()``, ``printDebugger()`` and ``clear()`` have a ``set`` as prefix followed by the previous method name. ``bcc()`` is now ``setBcc()`` and so on. -4. The config attributes in ``app/Config/Email.php`` have changed. You should have a look at the `Email Class Documentation `_ to have a list of the new attributes. +4. The config attributes in ``app/Config/Email.php`` have changed. You should have a look at the `Email Class Documentation `__ to have a list of the new attributes. Code Example ============ diff --git a/user_guide_src/source/installation/upgrade_encryption.rst b/user_guide_src/source/installation/upgrade_encryption.rst index ee1365cd5c42..4ca341017b5d 100644 --- a/user_guide_src/source/installation/upgrade_encryption.rst +++ b/user_guide_src/source/installation/upgrade_encryption.rst @@ -10,7 +10,7 @@ Documentations ============== - `Encryption Library Documentation Codeigniter 3.X `_ -- `Encryption Service Documentation Codeigniter 4.X `_ +- :doc:`Encryption Service Documentation Codeigniter 4.X ` What has been changed diff --git a/user_guide_src/source/installation/upgrade_file_upload.rst b/user_guide_src/source/installation/upgrade_file_upload.rst index 069164dd2f68..194c94374846 100644 --- a/user_guide_src/source/installation/upgrade_file_upload.rst +++ b/user_guide_src/source/installation/upgrade_file_upload.rst @@ -9,7 +9,7 @@ Upgrade Working with Uploaded Files Documentations ============== - `Output Class Documentation Codeigniter 3.X `_ -- `Working with Uploaded Files Documentation Codeigniter 4.X `_ +- :doc:`Working with Uploaded Files Documentation Codeigniter 4.X ` What has been changed ===================== diff --git a/user_guide_src/source/installation/upgrade_html_tables.rst b/user_guide_src/source/installation/upgrade_html_tables.rst index 82d75f143584..f8563c0ca9a5 100644 --- a/user_guide_src/source/installation/upgrade_html_tables.rst +++ b/user_guide_src/source/installation/upgrade_html_tables.rst @@ -10,7 +10,7 @@ Documentations ============== - `HTML Table Documentation Codeigniter 3.X `_ -- `HTML Table Documentation Codeigniter 4.X `_ +- :doc:`HTML Table Documentation Codeigniter 4.X ` What has been changed diff --git a/user_guide_src/source/installation/upgrade_localization.rst b/user_guide_src/source/installation/upgrade_localization.rst index 14c6f7099f44..c6336ac700d9 100644 --- a/user_guide_src/source/installation/upgrade_localization.rst +++ b/user_guide_src/source/installation/upgrade_localization.rst @@ -10,7 +10,7 @@ Documentations ============== - `Language Documentation Codeigniter 3.X `_ -- `Localization Documentation Codeigniter 4.X `_ +- :doc:`Localization Documentation Codeigniter 4.X ` What has been changed @@ -63,4 +63,3 @@ Codeigniter Version 4.x echo lang('Errors.errorEmailMissing'); - diff --git a/user_guide_src/source/installation/upgrade_pagination.rst b/user_guide_src/source/installation/upgrade_pagination.rst index bb5ffedbc632..a44240b8d4d5 100644 --- a/user_guide_src/source/installation/upgrade_pagination.rst +++ b/user_guide_src/source/installation/upgrade_pagination.rst @@ -10,7 +10,7 @@ Documentations ============== - `Pagination Class Documentation Codeigniter 3.X `_ -- `Pagination Documentation Codeigniter 4.X `_ +- :doc:`Pagination Documentation Codeigniter 4.X ` What has been changed @@ -59,4 +59,3 @@ Codeigniter Version 4.x ]; echo view('users/index', $data); - diff --git a/user_guide_src/source/installation/upgrade_responses.rst b/user_guide_src/source/installation/upgrade_responses.rst index c0ba918cbb68..a8ea5961b279 100644 --- a/user_guide_src/source/installation/upgrade_responses.rst +++ b/user_guide_src/source/installation/upgrade_responses.rst @@ -9,7 +9,7 @@ Upgrade HTTP Responses Documentations ============== - `Output Class Documentation Codeigniter 3.X `_ -- `HTTP Responses Documentation Codeigniter 4.X `_ +- :doc:`HTTP Responses Documentation Codeigniter 4.X ` What has been changed ===================== @@ -18,7 +18,7 @@ What has been changed Upgrade Guide ============= 1. The methods in the HTML Responses class are named slightly different. The most important change in the naming is the switch from underscored method names to camelCase. The method ``set_content_type()`` from version 3 is now named ``setContentType()`` and so on. -2. In the most cases you have to change ``$this->output`` to ``$this->response`` followed by the method. You can find all methods `here `_. +2. In the most cases you have to change ``$this->output`` to ``$this->response`` followed by the method. You can find all methods :doc:`here `. Code Example ============ diff --git a/user_guide_src/source/installation/upgrade_routing.rst b/user_guide_src/source/installation/upgrade_routing.rst index 575a0699561d..3c55995d8a17 100644 --- a/user_guide_src/source/installation/upgrade_routing.rst +++ b/user_guide_src/source/installation/upgrade_routing.rst @@ -10,7 +10,7 @@ Documentations ============== - `URI Routing Documentation Codeigniter 3.X `_ -- `URI Routing Documentation Codeigniter 4.X `_ +- :doc:`URI Routing Documentation Codeigniter 4.X ` What has been changed @@ -74,4 +74,3 @@ Path: ``app/Config/Routes.php``:: $routes->add('drivers/create', 'drivers::create'); $routes->add('drivers/edit/(:any)', 'drivers::edit/$1'); $routes->add('posts/(:any)', 'posts::view/$1'); - diff --git a/user_guide_src/source/installation/upgrade_security.rst b/user_guide_src/source/installation/upgrade_security.rst index 88f4814ddb12..e07331e8b752 100644 --- a/user_guide_src/source/installation/upgrade_security.rst +++ b/user_guide_src/source/installation/upgrade_security.rst @@ -10,7 +10,7 @@ Documentations ============== - `Security Class Documentation Codeigniter 3.X `_ -- `Security Documentation Codeigniter 4.X `_ +- :doc:`Security Documentation Codeigniter 4.X ` .. note:: If you use the `form helper `_, then form_open() will automatically insert a hidden csrf field in your forms. So you do not have to upgrade this by yourself. @@ -68,4 +68,3 @@ Codeigniter Version 4.x - diff --git a/user_guide_src/source/installation/upgrade_sessions.rst b/user_guide_src/source/installation/upgrade_sessions.rst index e3d7b4da343b..77e25a6fd091 100644 --- a/user_guide_src/source/installation/upgrade_sessions.rst +++ b/user_guide_src/source/installation/upgrade_sessions.rst @@ -10,7 +10,7 @@ Documentations ============== - `Session Library Documentation Codeigniter 3.X `_ -- `Session Library Documentation Codeigniter 4.X `_ +- :doc:`Session Library Documentation Codeigniter 4.X ` What has been changed @@ -50,4 +50,3 @@ Codeigniter Version 4.x $session->get('item'); $session->item; session('item'); - diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst index 020952a1d9c6..b0e58106d9f4 100644 --- a/user_guide_src/source/installation/upgrade_validations.rst +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -10,7 +10,7 @@ Documentations of Library ========================= - `Form Validation Documentation Codeigniter 3.X `_ -- `Validation Documentation Codeigniter 4.X `_ +- :doc:`Validation Documentation Codeigniter 4.X ` What has been changed @@ -156,4 +156,4 @@ Path: ``app/Controllers/``:: echo view('Success'); } } - } \ No newline at end of file + } diff --git a/user_guide_src/source/installation/upgrade_view_parser.rst b/user_guide_src/source/installation/upgrade_view_parser.rst index 3b23a4ea1d0b..b6c335fd3df5 100644 --- a/user_guide_src/source/installation/upgrade_view_parser.rst +++ b/user_guide_src/source/installation/upgrade_view_parser.rst @@ -10,7 +10,7 @@ Documentations ============== - `Template Parser Documentation Codeigniter 3.X `_ -- `View Parser Documentation Codeigniter 4.X `_ +- :doc:`View Parser Documentation Codeigniter 4.X ` What has been changed @@ -53,4 +53,3 @@ Codeigniter Version 4.x echo $parser->setData($data) ->render('blog_template'); - From 6192271268eedaac022f02f18a2dc5e1cd48427a Mon Sep 17 00:00:00 2001 From: nschmolke <79579458+nschmolke@users.noreply.github.com> Date: Mon, 19 Apr 2021 14:15:34 +0200 Subject: [PATCH 096/490] Apply suggestions from code review Co-authored-by: kenjis --- user_guide_src/source/installation/upgrade_file_upload.rst | 4 ++-- user_guide_src/source/installation/upgrade_localization.rst | 3 ++- user_guide_src/source/installation/upgrade_security.rst | 4 ++-- user_guide_src/source/installation/upgrade_validations.rst | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_file_upload.rst b/user_guide_src/source/installation/upgrade_file_upload.rst index 194c94374846..2bfbd99765fa 100644 --- a/user_guide_src/source/installation/upgrade_file_upload.rst +++ b/user_guide_src/source/installation/upgrade_file_upload.rst @@ -47,7 +47,7 @@ Codeigniter Version 3.11 public function do_upload() { $config['upload_path'] = './uploads/'; - $config['allowed_types'] = 'gif|jpg|png'; + $config['allowed_types'] = 'png|jpg|gif'; $config['max_size'] = 100; $config['max_width'] = 1024; $config['max_height'] = 768; @@ -88,7 +88,7 @@ Codeigniter Version 4.x public function do_upload() { $this->validate([ - 'userfile' => 'uploaded[userfile]|max_size[userfile,1024]|mime_in[userfile,image/png,image/jpg,image/gif]|max_dims[userfile,1024,768]' + 'userfile' => 'uploaded[userfile]|max_size[userfile,100]|mime_in[userfile,image/png,image/jpg,image/gif]|ext_in[userfile,png,jpg,gif]|max_dims[userfile,1024,768]' ]); $file = $this->request->getFile('userfile'); diff --git a/user_guide_src/source/installation/upgrade_localization.rst b/user_guide_src/source/installation/upgrade_localization.rst index c6336ac700d9..5c6125851a92 100644 --- a/user_guide_src/source/installation/upgrade_localization.rst +++ b/user_guide_src/source/installation/upgrade_localization.rst @@ -35,6 +35,7 @@ Codeigniter Version 3.11 ------------------------ :: + // error.php $lang['error_email_missing'] = 'You must submit an email address'; $lang['error_url_missing'] = 'You must submit a URL'; $lang['error_username_missing'] = 'You must submit a username'; @@ -48,6 +49,7 @@ Codeigniter Version 4.x ----------------------- :: + // Errors.php return [ 'errorEmailMissing' => 'You must submit an email address', 'errorURLMissing' => 'You must submit a URL', @@ -62,4 +64,3 @@ Codeigniter Version 4.x ... echo lang('Errors.errorEmailMissing'); - diff --git a/user_guide_src/source/installation/upgrade_security.rst b/user_guide_src/source/installation/upgrade_security.rst index e07331e8b752..7b8f51d4fbc0 100644 --- a/user_guide_src/source/installation/upgrade_security.rst +++ b/user_guide_src/source/installation/upgrade_security.rst @@ -30,7 +30,7 @@ Upgrade Guide ] ]; -2. Within you html forms you have to remove the csrf input which looks similar to ````. +2. Within you html forms you have to remove the csrf input which looks similar to ````. 3. Now within your html forms you have to add ```` somewhere in the form body, unless you are using `form_open()``. Code Example @@ -52,7 +52,7 @@ Codeigniter Version 3.11 - + diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst index b0e58106d9f4..44a7a9bd1e31 100644 --- a/user_guide_src/source/installation/upgrade_validations.rst +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -147,13 +147,13 @@ Path: ``app/Controllers/``:: if (! $this->validate([])) { - echo view('Signup', [ + echo view('myform', [ 'validation' => $this->validator, ]); } else { - echo view('Success'); + echo view('formsuccess'); } } } From e492274c4187e1f85d02b1d82639ac48cc3098ec Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Tue, 29 Jun 2021 23:46:37 -0500 Subject: [PATCH 097/490] Using common helper methods in place of fully qualified Service calls. --- user_guide_src/source/installation/upgrade_database.rst | 4 ++-- user_guide_src/source/installation/upgrade_emails.rst | 4 ++-- user_guide_src/source/installation/upgrade_encryption.rst | 4 ++-- user_guide_src/source/installation/upgrade_sessions.rst | 4 ++-- user_guide_src/source/installation/upgrade_view_parser.rst | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_database.rst b/user_guide_src/source/installation/upgrade_database.rst index 3472aef4d69e..0ebc8f75277f 100644 --- a/user_guide_src/source/installation/upgrade_database.rst +++ b/user_guide_src/source/installation/upgrade_database.rst @@ -21,8 +21,8 @@ What has been changed Upgrade Guide ============= 1. Add your database credentials to ``app/Config/Database.php``. The options are pretty much the same as in CI3 only some names have changed slightly. -2. Everywhere you have used the database you have to replace ``$this->load->database();`` with ``$db = \Config\Database::connect();``. -3. If you use multiple databases use the following code to load additional databases ``$db = \Config\Database::connect('group_name');``. +2. Everywhere you have used the database you have to replace ``$this->load->database();`` with ``$db = db_connect();``. +3. If you use multiple databases use the following code to load additional databases ``$db = db_connect('group_name');``. 4. Now you have to change all database queries. The most important change here is to replace ``$this->db`` with just ``$db`` and adjust the method name and ``$db``. Here are some examples: - ``$this->db->query('YOUR QUERY HERE');`` to ``$db->query('YOUR QUERY HERE');`` diff --git a/user_guide_src/source/installation/upgrade_emails.rst b/user_guide_src/source/installation/upgrade_emails.rst index 0915948afabd..1a65f279cfa7 100644 --- a/user_guide_src/source/installation/upgrade_emails.rst +++ b/user_guide_src/source/installation/upgrade_emails.rst @@ -19,7 +19,7 @@ What has been changed Upgrade Guide ============= -1. Within your class change the ``$this->load->library('email');`` to ``$email = \Config\Services::email();``. +1. Within your class change the ``$this->load->library('email');`` to ``$email = service('email');``. 2. From that on you have to replace every line starting with ``$this->email`` to ``$email``. 3. The methods in the Email class are named slightly different. All methods, except for ``send()``, ``attach()``, ``printDebugger()`` and ``clear()`` have a ``set`` as prefix followed by the previous method name. ``bcc()`` is now ``setBcc()`` and so on. 4. The config attributes in ``app/Config/Email.php`` have changed. You should have a look at the `Email Class Documentation `__ to have a list of the new attributes. @@ -47,7 +47,7 @@ Codeigniter Version 4.x ----------------------- :: - $email = \Config\Services::email(); + $email = service('email'); $email->setFrom('your@example.com', 'Your Name'); $email->setTo('someone@example.com'); diff --git a/user_guide_src/source/installation/upgrade_encryption.rst b/user_guide_src/source/installation/upgrade_encryption.rst index 4ca341017b5d..7dadbba63e85 100644 --- a/user_guide_src/source/installation/upgrade_encryption.rst +++ b/user_guide_src/source/installation/upgrade_encryption.rst @@ -20,7 +20,7 @@ What has been changed Upgrade Guide ============= 1. Within your configs the ``$config['encryption_key'] = 'abc123';`` moved from ``application/config/config.php`` to ``public $key = 'abc123';`` in ``app/Config/Encryption.php``. -2. Wherever you have used the encryption library you have to replace ``$this->load->library('encryption');`` with ``$encrypter = \Config\Services::encrypter();`` and change the methods for encryption and decrypting like in the following code example. +2. Wherever you have used the encryption library you have to replace ``$this->load->library('encryption');`` with ``$encrypter = service('encrypter');`` and change the methods for encryption and decrypting like in the following code example. Code Example ============ @@ -42,7 +42,7 @@ Codeigniter Version 4.x ----------------------- :: - $encrypter = \Config\Services::encrypter(); + $encrypter = service('encrypter'); $plainText = 'This is a plain-text message!'; $ciphertext = $encrypter->encrypt($plainText); diff --git a/user_guide_src/source/installation/upgrade_sessions.rst b/user_guide_src/source/installation/upgrade_sessions.rst index 77e25a6fd091..84757dbfd665 100644 --- a/user_guide_src/source/installation/upgrade_sessions.rst +++ b/user_guide_src/source/installation/upgrade_sessions.rst @@ -19,7 +19,7 @@ What has been changed Upgrade Guide ============= -1. Wherever you use the Session Library replace ``$this->load->library('session');`` with ``$session = \Config\Services::session();``. +1. Wherever you use the Session Library replace ``$this->load->library('session');`` with ``$session = session();``. 2. From that on you have to replace every line starting with ``$this->session`` with ``$session`` followed by the new method name. - To access session data use the syntax ``$session->item`` or ``$session->get('item')`` instead of the CI3 syntax ``$this->session->name``. @@ -44,7 +44,7 @@ Codeigniter Version 4.x ----------------------- :: - $session = \Config\Services::session(); + $session = session(); $_SESSION['item']; $session->get('item'); diff --git a/user_guide_src/source/installation/upgrade_view_parser.rst b/user_guide_src/source/installation/upgrade_view_parser.rst index b6c335fd3df5..f3147bbcd6fd 100644 --- a/user_guide_src/source/installation/upgrade_view_parser.rst +++ b/user_guide_src/source/installation/upgrade_view_parser.rst @@ -20,7 +20,7 @@ What has been changed Upgrade Guide ============= -1. Wherever you use the View Parser Library replace ``$this->load->library('parser');`` with ``$parser = \Config\Services::parser();``. +1. Wherever you use the View Parser Library replace ``$this->load->library('parser');`` with ``$parser = service('parser');``. 2. You have to change the render part in your controller from ``$this->parser->parse('blog_template', $data);`` to ``echo $parser->setData($data)->render('blog_template');``. Code Example @@ -44,7 +44,7 @@ Codeigniter Version 4.x ----------------------- :: - $parser = \Config\Services::parser(); + $parser = service('parser'); $data = [ 'blog_title' => 'My Blog Title', From 24ed44417d6f4d42e125a21753b2cdd1540b3595 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 12:19:29 +0900 Subject: [PATCH 098/490] docs: fix indentation of lists --- .../source/installation/upgrade_migrations.rst | 8 ++++---- .../source/installation/upgrade_pagination.rst | 4 ++-- .../source/installation/upgrade_validations.rst | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_migrations.rst b/user_guide_src/source/installation/upgrade_migrations.rst index f21243a2b565..13a0082ff983 100644 --- a/user_guide_src/source/installation/upgrade_migrations.rst +++ b/user_guide_src/source/installation/upgrade_migrations.rst @@ -30,10 +30,10 @@ Upgrade Guide 6. Replace ``extends CI_Migration`` with ``extends Migration``. 7. The method names within the ``Forge`` class has been changed to use camelCase. For example: -- ``$this->dbforge->add_field`` to ``$this->forge->addField`` -- ``$this->dbforge->add_key`` to ``$this->forge->addKey`` -- ``$this->dbforge->create_table`` to ``$this->forge->addTable`` -- ``$this->dbforge->drop_table`` to ``$this->forge->addTable`` + - ``$this->dbforge->add_field`` to ``$this->forge->addField`` + - ``$this->dbforge->add_key`` to ``$this->forge->addKey`` + - ``$this->dbforge->create_table`` to ``$this->forge->addTable`` + - ``$this->dbforge->drop_table`` to ``$this->forge->addTable`` 8. (optional) You can change the array syntax from ``array(...)`` to ``[...]`` diff --git a/user_guide_src/source/installation/upgrade_pagination.rst b/user_guide_src/source/installation/upgrade_pagination.rst index a44240b8d4d5..29fa4f968633 100644 --- a/user_guide_src/source/installation/upgrade_pagination.rst +++ b/user_guide_src/source/installation/upgrade_pagination.rst @@ -21,11 +21,11 @@ Upgrade Guide ============= 1. Within the views change to following: -- ``pagination->create_links(); ?>`` to ``links() ?>`` + - ``pagination->create_links(); ?>`` to ``links() ?>`` 2. Within the controller you have to make the following changes: -- You can use the built-in ``paginate()`` method on every Model. Have a look at the code example below to see how you setup the pagination on a specific model. + - You can use the built-in ``paginate()`` method on every Model. Have a look at the code example below to see how you setup the pagination on a specific model. Code Example diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst index 44a7a9bd1e31..da04af513351 100644 --- a/user_guide_src/source/installation/upgrade_validations.rst +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -21,15 +21,15 @@ Upgrade Guide ============= 1. Within the view which contains the form you have to change: -- ```` to ``listErrors() ?>`` -- ```` to ```` + - ```` to ``listErrors() ?>`` + - ```` to ```` 2. Within the controller you have to change the following: -- ``$this->load->helper(array('form', 'url'));`` to ``helper(['form', 'url']);`` -- remove the line ``$this->load->library('form_validation');`` -- ``if($this->form_validation->run() == FALSE)`` to ``if (! $this->validate([]))`` -- ``$this->load->view('myform');`` to ``echo view('myform', ['validation' => $this->validator,]);`` + - ``$this->load->helper(array('form', 'url'));`` to ``helper(['form', 'url']);`` + - remove the line ``$this->load->library('form_validation');`` + - ``if($this->form_validation->run() == FALSE)`` to ``if (! $this->validate([]))`` + - ``$this->load->view('myform');`` to ``echo view('myform', ['validation' => $this->validator,]);`` 3. You have to change the validation rules. The new syntax is to set the rules as array in the controller:: From a407185d98b74d3059ebafe6b18003787da65b7f Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 12:19:54 +0900 Subject: [PATCH 099/490] docs: add links to each upgrade page --- .../source/installation/upgrade_4xx.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_4xx.rst b/user_guide_src/source/installation/upgrade_4xx.rst index 676e76110306..dcf19217b277 100644 --- a/user_guide_src/source/installation/upgrade_4xx.rst +++ b/user_guide_src/source/installation/upgrade_4xx.rst @@ -134,8 +134,21 @@ Upgrading Libraries .. toctree:: :titlesonly: - upgrade_migrations upgrade_configuration + upgrade_database + upgrade_emails + upgrade_encryption + upgrade_file_upload + upgrade_html_tables + upgrade_localization + upgrade_migrations + upgrade_pagination + upgrade_responses + upgrade_routing + upgrade_security + upgrade_sessions + upgrade_validations + upgrade_view_parser .. note:: More upgrade guides coming soon From a5ff5a7ea7dae0c1f633ab75d9f9d2c2886e67e7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 12:21:05 +0900 Subject: [PATCH 100/490] docs: replace HTML with HTTP https://github.com/codeigniter4/CodeIgniter4/pull/4565#discussion_r633463661 --- user_guide_src/source/installation/upgrade_responses.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_responses.rst b/user_guide_src/source/installation/upgrade_responses.rst index a8ea5961b279..15cf10aae0a7 100644 --- a/user_guide_src/source/installation/upgrade_responses.rst +++ b/user_guide_src/source/installation/upgrade_responses.rst @@ -17,7 +17,7 @@ What has been changed Upgrade Guide ============= -1. The methods in the HTML Responses class are named slightly different. The most important change in the naming is the switch from underscored method names to camelCase. The method ``set_content_type()`` from version 3 is now named ``setContentType()`` and so on. +1. The methods in the HTTP Responses class are named slightly different. The most important change in the naming is the switch from underscored method names to camelCase. The method ``set_content_type()`` from version 3 is now named ``setContentType()`` and so on. 2. In the most cases you have to change ``$this->output`` to ``$this->response`` followed by the method. You can find all methods :doc:`here `. Code Example From c04075456e0cd3c0f8429143f5941a4985ca76df Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 12:26:10 +0900 Subject: [PATCH 101/490] docs: remove unneeded code --- user_guide_src/source/installation/upgrade_responses.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_responses.rst b/user_guide_src/source/installation/upgrade_responses.rst index 15cf10aae0a7..6b5cc1b4ef7b 100644 --- a/user_guide_src/source/installation/upgrade_responses.rst +++ b/user_guide_src/source/installation/upgrade_responses.rst @@ -39,8 +39,7 @@ Codeigniter Version 4.x ----------------------- :: - $this->response->setStatusCode(404) - ->setBody($body); + $this->response->setStatusCode(404); ... From afb1832a2602e1efa5dcd75fe5401e13e4ae0e71 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 13:19:10 +0900 Subject: [PATCH 102/490] docs: remove unneeded change instruction --- user_guide_src/source/installation/upgrade_validations.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst index da04af513351..b518e4fd8b36 100644 --- a/user_guide_src/source/installation/upgrade_validations.rst +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -22,7 +22,6 @@ Upgrade Guide 1. Within the view which contains the form you have to change: - ```` to ``listErrors() ?>`` - - ```` to ```` 2. Within the controller you have to change the following: From e9669b7fd59c6f1a616625a0c366e8b9015afe1a Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 13:20:37 +0900 Subject: [PATCH 103/490] docs: fix coding style --- user_guide_src/source/installation/upgrade_validations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst index b518e4fd8b36..e5029a1b012e 100644 --- a/user_guide_src/source/installation/upgrade_validations.rst +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -27,7 +27,7 @@ Upgrade Guide - ``$this->load->helper(array('form', 'url'));`` to ``helper(['form', 'url']);`` - remove the line ``$this->load->library('form_validation');`` - - ``if($this->form_validation->run() == FALSE)`` to ``if (! $this->validate([]))`` + - ``if ($this->form_validation->run() == FALSE)`` to ``if (! $this->validate([]))`` - ``$this->load->view('myform');`` to ``echo view('myform', ['validation' => $this->validator,]);`` 3. You have to change the validation rules. The new syntax is to set the rules as array in the controller:: From f0b4ba4e91ea64737b77670ef7c61490d02ea638 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 13:28:30 +0900 Subject: [PATCH 104/490] docs: add about validation rule setting --- user_guide_src/source/installation/upgrade_validations.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst index e5029a1b012e..d498da5eaedc 100644 --- a/user_guide_src/source/installation/upgrade_validations.rst +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -86,6 +86,8 @@ Path: ``application/controllers/``:: $this->load->library('form_validation'); + // Set validation rules + if ($this->form_validation->run() == FALSE) { $this->load->view('myform'); @@ -144,8 +146,9 @@ Path: ``app/Controllers/``:: { helper(['form', 'url']); - if (! $this->validate([])) - { + if (! $this->validate([ + // Validation rules + ])) { echo view('myform', [ 'validation' => $this->validator, ]); From 2479e2d36570ccb0f45b7defe164424829f7e86d Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 13:29:43 +0900 Subject: [PATCH 105/490] docs: rename variable to more appropriate name --- user_guide_src/source/installation/upgrade_validations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst index d498da5eaedc..3dddcf177ff1 100644 --- a/user_guide_src/source/installation/upgrade_validations.rst +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -32,8 +32,8 @@ Upgrade Guide 3. You have to change the validation rules. The new syntax is to set the rules as array in the controller:: - $input = $this->validate([ 'name' => 'required|min_length[3]', + $isValid = $this->validate([ 'email' => 'required|valid_email', 'phone' => 'required|numeric|max_length[10]' ]); From 3b02a1e0d2d86d3c4c83a400e1f9163fe37ad42d Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 13:31:24 +0900 Subject: [PATCH 106/490] docs: fix coding style --- user_guide_src/source/installation/upgrade_validations.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst index 3dddcf177ff1..f26cd53d1b5a 100644 --- a/user_guide_src/source/installation/upgrade_validations.rst +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -32,8 +32,8 @@ Upgrade Guide 3. You have to change the validation rules. The new syntax is to set the rules as array in the controller:: - 'name' => 'required|min_length[3]', $isValid = $this->validate([ + 'name' => 'required|min_length[3]', 'email' => 'required|valid_email', 'phone' => 'required|numeric|max_length[10]' ]); @@ -152,9 +152,7 @@ Path: ``app/Controllers/``:: echo view('myform', [ 'validation' => $this->validator, ]); - } - else - { + } else { echo view('formsuccess'); } } From 9af8a8952cc5bfed67e70f98f4e7d61effbe34d5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 13:39:06 +0900 Subject: [PATCH 107/490] docs: fix and add "What has been changed" --- user_guide_src/source/installation/upgrade_pagination.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_pagination.rst b/user_guide_src/source/installation/upgrade_pagination.rst index 29fa4f968633..012e0c6b7544 100644 --- a/user_guide_src/source/installation/upgrade_pagination.rst +++ b/user_guide_src/source/installation/upgrade_pagination.rst @@ -15,7 +15,10 @@ Documentations What has been changed ===================== -- In CI4 the pagination is a built-in method in the Model class. You have to change the views and also the controller in order to use the new pagination library. +- You have to change the views and also the controller in order to use the new pagination library. +- If you want to customize the pagination links, you need to create View Templates. +- In CI4 the pagination uses the actual page number only. You can't use the starting index (offset) for the items which is the default in CI3. +- If you use ``CodeIgniter\Model``, you can use the built-in method in the Model class. Upgrade Guide ============= From be5f610970692e3fcd358429465f81ee2a64c5d2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 13:46:54 +0900 Subject: [PATCH 108/490] docs: add link to CodeIgniter\Model --- user_guide_src/source/installation/upgrade_pagination.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_pagination.rst b/user_guide_src/source/installation/upgrade_pagination.rst index 012e0c6b7544..8151e82465e5 100644 --- a/user_guide_src/source/installation/upgrade_pagination.rst +++ b/user_guide_src/source/installation/upgrade_pagination.rst @@ -18,7 +18,7 @@ What has been changed - You have to change the views and also the controller in order to use the new pagination library. - If you want to customize the pagination links, you need to create View Templates. - In CI4 the pagination uses the actual page number only. You can't use the starting index (offset) for the items which is the default in CI3. -- If you use ``CodeIgniter\Model``, you can use the built-in method in the Model class. +- If you use :doc:`CodeIgnite\\Model `, you can use the built-in method in the Model class. Upgrade Guide ============= From 81658aaa77f84250c3524c3bdb6bb3fbe96fe061 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 13:47:38 +0900 Subject: [PATCH 109/490] docs: fix coding style --- user_guide_src/source/installation/upgrade_routing.rst | 3 +-- user_guide_src/source/installation/upgrade_security.rst | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_routing.rst b/user_guide_src/source/installation/upgrade_routing.rst index 3c55995d8a17..4b30b24f7f2c 100644 --- a/user_guide_src/source/installation/upgrade_routing.rst +++ b/user_guide_src/source/installation/upgrade_routing.rst @@ -58,8 +58,7 @@ Path: ``app/Config/Routes.php``:: // Load the system's routing file first, so that the app and ENVIRONMENT // can override as needed. - if (file_exists(SYSTEMPATH . 'Config/Routes.php')) - { + if (file_exists(SYSTEMPATH . 'Config/Routes.php')) { require SYSTEMPATH . 'Config/Routes.php'; } diff --git a/user_guide_src/source/installation/upgrade_security.rst b/user_guide_src/source/installation/upgrade_security.rst index 7b8f51d4fbc0..d272a7609236 100644 --- a/user_guide_src/source/installation/upgrade_security.rst +++ b/user_guide_src/source/installation/upgrade_security.rst @@ -25,8 +25,8 @@ Upgrade Guide public $globals = [ 'before' => [ - //'honeypot' - 'csrf' + //'honeypot', + 'csrf', ] ]; @@ -41,8 +41,8 @@ Codeigniter Version 3.11 :: $csrf = array( - 'name' => $this->security->get_csrf_token_name(), - 'hash' => $this->security->get_csrf_hash() + 'name' => $this->security->get_csrf_token_name(), + 'hash' => $this->security->get_csrf_hash() ); ... From a8380fa724021e977b1104ca937cb99bbbe30146 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 13:47:53 +0900 Subject: [PATCH 110/490] docs: add "What has been changed" --- user_guide_src/source/installation/upgrade_validations.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst index f26cd53d1b5a..d3301c4cb37f 100644 --- a/user_guide_src/source/installation/upgrade_validations.rst +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -15,7 +15,10 @@ Documentations of Library What has been changed ===================== - +- If you want to change validation error display, you have to set CI4 validation View templates. +- CI4 validation has no Callbacks nor Callable in CI3. +- CI4 validation format rules do not permit empty string. +- CI4 validation never changes your data. Upgrade Guide ============= From 1f74b171e76ab6564b54f87cffe48a0d718f32f8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 13:49:17 +0900 Subject: [PATCH 111/490] docs: fix coding style --- user_guide_src/source/installation/upgrade_security.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_security.rst b/user_guide_src/source/installation/upgrade_security.rst index d272a7609236..d72de56f334f 100644 --- a/user_guide_src/source/installation/upgrade_security.rst +++ b/user_guide_src/source/installation/upgrade_security.rst @@ -25,8 +25,8 @@ Upgrade Guide public $globals = [ 'before' => [ - //'honeypot', - 'csrf', + //'honeypot', + 'csrf', ] ]; From 1a7ec9bfb66859060a5c80d92f29cb5d192e6d06 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 14:05:39 +0900 Subject: [PATCH 112/490] docs: fix indentation of lists --- .../source/installation/upgrade_database.rst | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_database.rst b/user_guide_src/source/installation/upgrade_database.rst index 0ebc8f75277f..798b24a1c6c0 100644 --- a/user_guide_src/source/installation/upgrade_database.rst +++ b/user_guide_src/source/installation/upgrade_database.rst @@ -25,22 +25,22 @@ Upgrade Guide 3. If you use multiple databases use the following code to load additional databases ``$db = db_connect('group_name');``. 4. Now you have to change all database queries. The most important change here is to replace ``$this->db`` with just ``$db`` and adjust the method name and ``$db``. Here are some examples: -- ``$this->db->query('YOUR QUERY HERE');`` to ``$db->query('YOUR QUERY HERE');`` -- ``$this->db->simple_query('YOUR QUERY')`` to ``$db->simpleQuery('YOUR QUERY')`` -- ``$this->db->escape("something")`` to ``$db->escape("something");`` -- ``$this->db->affected_rows();`` to ``$db->affectedRows();`` -- ``$query->result();`` to ``$query->getResult();`` -- ``$query->result_array();`` to ``$query->getResultArray();`` -- ``echo $this->db->count_all('my_table');`` to ``echo $db->table('my_table')->countAll();`` + - ``$this->db->query('YOUR QUERY HERE');`` to ``$db->query('YOUR QUERY HERE');`` + - ``$this->db->simple_query('YOUR QUERY')`` to ``$db->simpleQuery('YOUR QUERY')`` + - ``$this->db->escape("something")`` to ``$db->escape("something");`` + - ``$this->db->affected_rows();`` to ``$db->affectedRows();`` + - ``$query->result();`` to ``$query->getResult();`` + - ``$query->result_array();`` to ``$query->getResultArray();`` + - ``echo $this->db->count_all('my_table');`` to ``echo $db->table('my_table')->countAll();`` 5. To use the new Query Builder Class you have to initialize the builder ``$builder = $db->table('mytable');`` after that you can run the queries on the ``$builder``. Here are some examples: -- ``$this->db->get()`` to ``$builder->get();`` -- ``$this->db->get_where('mytable', array('id' => $id), $limit, $offset);`` to ``$builder->getWhere(['id' => $id], $limit, $offset);`` -- ``$this->db->select('title, content, date');`` to ``$builder->select('title, content, date');`` -- ``$this->db->select_max('age');`` to ``$builder->selectMax('age');`` -- ``$this->db->join('comments', 'comments.id = blogs.id');`` to ``$builder->join('comments', 'comments.id = blogs.id');`` -- ``$this->db->having('user_id', 45);`` to ``$builder->having('user_id', 45);`` + - ``$this->db->get()`` to ``$builder->get();`` + - ``$this->db->get_where('mytable', array('id' => $id), $limit, $offset);`` to ``$builder->getWhere(['id' => $id], $limit, $offset);`` + - ``$this->db->select('title, content, date');`` to ``$builder->select('title, content, date');`` + - ``$this->db->select_max('age');`` to ``$builder->selectMax('age');`` + - ``$this->db->join('comments', 'comments.id = blogs.id');`` to ``$builder->join('comments', 'comments.id = blogs.id');`` + - ``$this->db->having('user_id', 45);`` to ``$builder->having('user_id', 45);`` Code Example From dea1374475832220576fcd6b22ae7b1f03725e74 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 14:05:55 +0900 Subject: [PATCH 113/490] docs: fix RST format --- user_guide_src/source/installation/upgrade_security.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_security.rst b/user_guide_src/source/installation/upgrade_security.rst index d72de56f334f..4f2b8107a96b 100644 --- a/user_guide_src/source/installation/upgrade_security.rst +++ b/user_guide_src/source/installation/upgrade_security.rst @@ -31,7 +31,7 @@ Upgrade Guide ]; 2. Within you html forms you have to remove the csrf input which looks similar to ````. -3. Now within your html forms you have to add ```` somewhere in the form body, unless you are using `form_open()``. +3. Now within your html forms you have to add ```` somewhere in the form body, unless you are using ``form_open()``. Code Example ============ From 2359ae455736516b28c12e4db4993964d003b1f4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 14:06:25 +0900 Subject: [PATCH 114/490] docs: fix coding style --- .../installation/upgrade_file_upload.rst | 103 +++++++++--------- .../installation/upgrade_localization.rst | 4 +- .../installation/upgrade_validations.rst | 26 ++--- 3 files changed, 65 insertions(+), 68 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_file_upload.rst b/user_guide_src/source/installation/upgrade_file_upload.rst index 2bfbd99765fa..e93884f7fa81 100644 --- a/user_guide_src/source/installation/upgrade_file_upload.rst +++ b/user_guide_src/source/installation/upgrade_file_upload.rst @@ -33,42 +33,41 @@ Codeigniter Version 3.11 class Upload extends CI_Controller { - public function __construct() + public function __construct() + { + parent::__construct(); + $this->load->helper(array('form', 'url')); + } + + public function index() + { + $this->load->view('upload_form', array('error' => ' ' )); + } + + public function do_upload() + { + $config['upload_path'] = './uploads/'; + $config['allowed_types'] = 'png|jpg|gif'; + $config['max_size'] = 100; + $config['max_width'] = 1024; + $config['max_height'] = 768; + + $this->load->library('upload', $config); + + if ( ! $this->upload->do_upload('userfile')) { - parent::__construct(); - $this->load->helper(array('form', 'url')); - } + $error = array('error' => $this->upload->display_errors()); - public function index() - { - $this->load->view('upload_form', array('error' => ' ' )); + $this->load->view('upload_form', $error); } - - public function do_upload() + else { - $config['upload_path'] = './uploads/'; - $config['allowed_types'] = 'png|jpg|gif'; - $config['max_size'] = 100; - $config['max_width'] = 1024; - $config['max_height'] = 768; - - $this->load->library('upload', $config); - - if ( ! $this->upload->do_upload('userfile')) - { - $error = array('error' => $this->upload->display_errors()); - - $this->load->view('upload_form', $error); - } - else - { - $data = array('upload_data' => $this->upload->data()); - - $this->load->view('upload_success', $data); - } + $data = array('upload_data' => $this->upload->data()); + + $this->load->view('upload_success', $data); } + } } - ?> Codeigniter Version 4.x ----------------------- @@ -80,29 +79,27 @@ Codeigniter Version 4.x class Upload extends BaseController { - public function index() - { - echo view('upload_form', ['error' => ' ']); - } + public function index() + { + echo view('upload_form', ['error' => ' ']); + } - public function do_upload() - { - $this->validate([ - 'userfile' => 'uploaded[userfile]|max_size[userfile,100]|mime_in[userfile,image/png,image/jpg,image/gif]|ext_in[userfile,png,jpg,gif]|max_dims[userfile,1024,768]' - ]); - - $file = $this->request->getFile('userfile'); - - if ( ! $path = $file->store()) - { - echo view('upload_form', ['error' => "upload failed"]); - } - else - { - $data = ['upload_file_path' => $path]; - - echo view('upload_success', $data); - } + public function do_upload() + { + $this->validate([ + 'userfile' => 'uploaded[userfile]|max_size[userfile,100]' + . '|mime_in[userfile,image/png,image/jpg,image/gif]' + . '|ext_in[userfile,png,jpg,gif]|max_dims[userfile,1024,768]' + ]); + + $file = $this->request->getFile('userfile'); + + if (! $path = $file->store()) { + echo view('upload_form', ['error' => "upload failed"]); + } else { + $data = ['upload_file_path' => $path]; + + echo view('upload_success', $data); } + } } - ?> diff --git a/user_guide_src/source/installation/upgrade_localization.rst b/user_guide_src/source/installation/upgrade_localization.rst index 5c6125851a92..bb01d0057228 100644 --- a/user_guide_src/source/installation/upgrade_localization.rst +++ b/user_guide_src/source/installation/upgrade_localization.rst @@ -36,8 +36,8 @@ Codeigniter Version 3.11 :: // error.php - $lang['error_email_missing'] = 'You must submit an email address'; - $lang['error_url_missing'] = 'You must submit a URL'; + $lang['error_email_missing'] = 'You must submit an email address'; + $lang['error_url_missing'] = 'You must submit a URL'; $lang['error_username_missing'] = 'You must submit a username'; ... diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst index d3301c4cb37f..0bc0928ee32e 100644 --- a/user_guide_src/source/installation/upgrade_validations.rst +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -83,23 +83,23 @@ Path: ``application/controllers/``:: class Form extends CI_Controller { - public function index() - { - $this->load->helper(array('form', 'url')); + public function index() + { + $this->load->helper(array('form', 'url')); - $this->load->library('form_validation'); + $this->load->library('form_validation'); - // Set validation rules + // Set validation rules - if ($this->form_validation->run() == FALSE) - { - $this->load->view('myform'); - } - else - { - $this->load->view('formsuccess'); - } + if ($this->form_validation->run() == FALSE) + { + $this->load->view('myform'); } + else + { + $this->load->view('formsuccess'); + } + } } Codeigniter Version 4.x From c453fa7152249885867c369bec12b505cd3c9a95 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 15:35:59 +0900 Subject: [PATCH 115/490] docs: small fixes for entities --- user_guide_src/source/models/entities.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/models/entities.rst b/user_guide_src/source/models/entities.rst index eec2447adf06..10828c0dc56b 100644 --- a/user_guide_src/source/models/entities.rst +++ b/user_guide_src/source/models/entities.rst @@ -70,7 +70,7 @@ Create the model first at **app/Models/UserModel.php** so that we can interact w protected $allowedFields = [ 'username', 'email', 'password', ]; - protected $returnType = 'App\Entities\User'; + protected $returnType = \App\Entities\User::class; protected $useTimestamps = true; } @@ -464,7 +464,7 @@ Now you need to register it:: //Bind the type to the handler protected $castHandlers = [ - 'base64' => 'App\Entity\Cast\CastBase64', + 'base64' => \App\Entity\Cast\CastBase64::class, ]; } @@ -496,16 +496,18 @@ Additional parameters are indicated in square brackets and listed with a comma. :: - //Defining a type with parameters + // Defining a type with parameters protected $casts = [ 'some_attribute' => 'class[App\SomeClass, param2, param3]', ]; - //Bind the type to the handler + // Bind the type to the handler protected $castHandlers = [ 'class' => 'SomeHandler', ]; +:: + use CodeIgniter\Entity\Cast\BaseCast; class SomeHandler extends BaseCast @@ -535,7 +537,7 @@ Checking for Changed Attributes You can check if an Entity attribute has changed since it was created. The only parameter is the name of the attribute to check:: - $user = new User(); + $user = new \App\Entities\User(); $user->hasChanged('name'); // false $user->name = 'Fred'; From 9d1570ae60f5e03bd4cd3f06bd998819b4d59027 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 16:10:26 +0900 Subject: [PATCH 116/490] docs: fix db_forge --- user_guide_src/source/dbmgmt/forge.rst | 386 +++++++++++++------------ 1 file changed, 199 insertions(+), 187 deletions(-) diff --git a/user_guide_src/source/dbmgmt/forge.rst b/user_guide_src/source/dbmgmt/forge.rst index e16b5ee1e92f..6cbdfe506e8a 100644 --- a/user_guide_src/source/dbmgmt/forge.rst +++ b/user_guide_src/source/dbmgmt/forge.rst @@ -13,16 +13,16 @@ Initializing the Forge Class **************************** .. important:: In order to initialize the Forge class, your database - driver must already be running, since the forge class relies on it. + driver must already be running, since the forge class relies on it. Load the Forge Class as follows:: - $forge = \Config\Database::forge(); + $forge = \Config\Database::forge(); You can also pass another database group name to the DB Forge loader, in case the database you want to manage isn't the default one:: - $this->myforge = \Config\Database::forge('other_db'); + $this->myforge = \Config\Database::forge('other_db'); In the above example, we're passing the name of a different database group to connect to as the first parameter. @@ -36,27 +36,27 @@ Creating and Dropping Databases Permits you to create the database specified in the first parameter. Returns true/false based on success or failure:: - if ($forge->createDatabase('my_db')) { - echo 'Database created!'; - } + if ($forge->createDatabase('my_db')) { + echo 'Database created!'; + } An optional second parameter set to true will add IF EXISTS statement or will check if a database exists before create it (depending on DBMS). :: - $forge->createDatabase('my_db', true); - // gives CREATE DATABASE IF NOT EXISTS my_db - // or will check if a database exists + $forge->createDatabase('my_db', true); + // gives CREATE DATABASE IF NOT EXISTS my_db + // or will check if a database exists **$forge->dropDatabase('db_name')** Permits you to drop the database specified in the first parameter. Returns true/false based on success or failure:: - if ($forge->dropDatabase('my_db')) { - echo 'Database deleted!'; - } + if ($forge->dropDatabase('my_db')) { + echo 'Database deleted!'; + } Creating Databases in the Command Line ====================================== @@ -67,7 +67,7 @@ will complain that the database creation has failed. To start, just type the command and the name of the database (e.g., ``foo``):: - php spark db:create foo + php spark db:create foo If everything went fine, you should expect the ``Database "foo" successfully created.`` message displayed. @@ -76,12 +76,12 @@ for the file where the database will be created using the ``--ext`` option. Vali ``sqlite`` and defaults to ``db``. Remember that these should not be preceded by a period. :: - php spark db:create foo --ext sqlite - // will create the db file in WRITEPATH/foo.sqlite + php spark db:create foo --ext sqlite + // will create the db file in WRITEPATH/foo.sqlite .. note:: When using the special SQLite3 database name ``:memory:``, expect that the command will still - produce a success message but no database file is created. This is because SQLite3 will just use - an in-memory database. + produce a success message but no database file is created. This is because SQLite3 will just use + an in-memory database. **************************** Creating and Dropping Tables @@ -101,13 +101,13 @@ also require a 'constraint' key. :: - $fields = [ - 'users' => [ - 'type' => 'VARCHAR', - 'constraint' => 100, - ], - ]; - // will translate to "users VARCHAR(100)" when the field is added. + $fields = [ + 'users' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + ], + ]; + // will translate to "users VARCHAR(100)" when the field is added. Additionally, the following key/values can be used: @@ -122,33 +122,33 @@ Additionally, the following key/values can be used: :: - $fields = [ - 'id' => [ - 'type' => 'INT', - 'constraint' => 5, - 'unsigned' => true, - 'auto_increment' => true - ], - 'title' => [ - 'type' => 'VARCHAR', - 'constraint' => '100', - 'unique' => true, - ], - 'author' => [ - 'type' =>'VARCHAR', - 'constraint' => 100, - 'default' => 'King of Town', - ], - 'description' => [ - 'type' => 'TEXT', - 'null' => true, - ], - 'status' => [ - 'type' => 'ENUM', - 'constraint' => ['publish', 'pending', 'draft'], - 'default' => 'pending', - ], - ]; + $fields = [ + 'id' => [ + 'type' => 'INT', + 'constraint' => 5, + 'unsigned' => true, + 'auto_increment' => true + ], + 'title' => [ + 'type' => 'VARCHAR', + 'constraint' => '100', + 'unique' => true, + ], + 'author' => [ + 'type' =>'VARCHAR', + 'constraint' => 100, + 'default' => 'King of Town', + ], + 'description' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'status' => [ + 'type' => 'ENUM', + 'constraint' => ['publish', 'pending', 'draft'], + 'default' => 'pending', + ], + ]; After the fields have been defined, they can be added using ``$forge->addField($fields);`` followed by a call to the @@ -166,7 +166,7 @@ string into the field definitions with addField() :: - $forge->addField("label varchar(100) NOT NULL DEFAULT 'default label'"); + $forge->addField("label varchar(100) NOT NULL DEFAULT 'default label'"); .. note:: Passing raw strings as fields cannot be followed by ``addKey()`` calls on those fields. @@ -181,8 +181,8 @@ Primary Key. :: - $forge->addField('id'); - // gives id INT(9) NOT NULL AUTO_INCREMENT + $forge->addField('id'); + // gives id INT(9) NOT NULL AUTO_INCREMENT Adding Keys =========== @@ -198,30 +198,30 @@ below is for MySQL. :: - $forge->addKey('blog_id', true); - // gives PRIMARY KEY `blog_id` (`blog_id`) + $forge->addKey('blog_id', true); + // gives PRIMARY KEY `blog_id` (`blog_id`) - $forge->addKey('blog_id', true); - $forge->addKey('site_id', true); - // gives PRIMARY KEY `blog_id_site_id` (`blog_id`, `site_id`) + $forge->addKey('blog_id', true); + $forge->addKey('site_id', true); + // gives PRIMARY KEY `blog_id_site_id` (`blog_id`, `site_id`) - $forge->addKey('blog_name'); - // gives KEY `blog_name` (`blog_name`) + $forge->addKey('blog_name'); + // gives KEY `blog_name` (`blog_name`) - $forge->addKey(['blog_name', 'blog_label']); - // gives KEY `blog_name_blog_label` (`blog_name`, `blog_label`) + $forge->addKey(['blog_name', 'blog_label']); + // gives KEY `blog_name_blog_label` (`blog_name`, `blog_label`) - $forge->addKey(['blog_id', 'uri'], false, true); - // gives UNIQUE KEY `blog_id_uri` (`blog_id`, `uri`) + $forge->addKey(['blog_id', 'uri'], false, true); + // gives UNIQUE KEY `blog_id_uri` (`blog_id`, `uri`) To make code reading more objective it is also possible to add primary and unique keys with specific methods:: - $forge->addPrimaryKey('blog_id'); - // gives PRIMARY KEY `blog_id` (`blog_id`) + $forge->addPrimaryKey('blog_id'); + // gives PRIMARY KEY `blog_id` (`blog_id`) - $forge->addUniqueKey(['blog_id', 'uri']); - // gives UNIQUE KEY `blog_id_uri` (`blog_id`, `uri`) + $forge->addUniqueKey(['blog_id', 'uri']); + // gives UNIQUE KEY `blog_id_uri` (`blog_id`, `uri`) Adding Foreign Keys @@ -230,19 +230,19 @@ Adding Foreign Keys Foreign Keys help to enforce relationships and actions across your tables. For tables that support Foreign Keys, you may add them directly in forge:: - $forge->addForeignKey('users_id','users','id'); - // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) + $forge->addForeignKey('users_id','users','id'); + // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) - $forge->addForeignKey(['users_id', 'users_name'],'users',['id', 'name']); - // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) + $forge->addForeignKey(['users_id', 'users_name'],'users',['id', 'name']); + // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) You can specify the desired action for the "on delete" and "on update" properties of the constraint:: - $forge->addForeignKey('users_id','users','id','CASCADE','CASCADE'); - // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE + $forge->addForeignKey('users_id','users','id','CASCADE','CASCADE'); + // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE - $forge->addForeignKey(['users_id', 'users_name'],'users',['id', 'name'],'CASCADE','CASCADE'); - // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) ON DELETE CASCADE ON UPDATE CASCADE + $forge->addForeignKey(['users_id', 'users_name'],'users',['id', 'name'],'CASCADE','CASCADE'); + // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) ON DELETE CASCADE ON UPDATE CASCADE Creating a table ================ @@ -252,26 +252,26 @@ with :: - $forge->createTable('table_name'); - // gives CREATE TABLE table_name + $forge->createTable('table_name'); + // gives CREATE TABLE table_name An optional second parameter set to true adds an "IF NOT EXISTS" clause into the definition :: - $forge->createTable('table_name', true); - // gives CREATE TABLE IF NOT EXISTS table_name + $forge->createTable('table_name', true); + // gives CREATE TABLE IF NOT EXISTS table_name You could also pass optional table attributes, such as MySQL's ``ENGINE``:: - $attributes = ['ENGINE' => 'InnoDB']; - $forge->createTable('table_name', false, $attributes); - // produces: CREATE TABLE `table_name` (...) ENGINE = InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci + $attributes = ['ENGINE' => 'InnoDB']; + $forge->createTable('table_name', false, $attributes); + // produces: CREATE TABLE `table_name` (...) ENGINE = InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci .. note:: Unless you specify the ``CHARACTER SET`` and/or ``COLLATE`` attributes, - ``createTable()`` will always add them with your configured *charset* - and *DBCollat* values, as long as they are not empty (MySQL only). + ``createTable()`` will always add them with your configured *charset* + and *DBCollat* values, as long as they are not empty (MySQL only). Dropping a table ================ @@ -280,19 +280,19 @@ Execute a DROP TABLE statement and optionally add an IF EXISTS clause. :: - // Produces: DROP TABLE table_name - $forge->dropTable('table_name'); + // Produces: DROP TABLE table_name + $forge->dropTable('table_name'); - // Produces: DROP TABLE IF EXISTS table_name - $forge->dropTable('table_name', true); + // Produces: DROP TABLE IF EXISTS table_name + $forge->dropTable('table_name', true); A third parameter can be passed to add a "CASCADE" option, which might be required for some drivers to handle removal of tables with foreign keys. :: - // Produces: DROP TABLE table_name CASCADE - $forge->dropTable('table_name', false, true); + // Produces: DROP TABLE table_name CASCADE + $forge->dropTable('table_name', false, true); Dropping a Foreign Key ====================== @@ -301,8 +301,8 @@ Execute a DROP FOREIGN KEY. :: - // Produces: ALTER TABLE 'tablename' DROP FOREIGN KEY 'users_foreign' - $forge->dropForeignKey('tablename','users_foreign'); + // Produces: ALTER TABLE 'tablename' DROP FOREIGN KEY 'users_foreign' + $forge->dropForeignKey('tablename','users_foreign'); Renaming a table ================ @@ -311,8 +311,8 @@ Executes a TABLE rename :: - $forge->renameTable('old_table_name', 'new_table_name'); - // gives ALTER TABLE old_table_name RENAME TO new_table_name + $forge->renameTable('old_table_name', 'new_table_name'); + // gives ALTER TABLE old_table_name RENAME TO new_table_name **************** Modifying Tables @@ -329,26 +329,26 @@ number of additional fields. :: - $fields = [ - 'preferences' => ['type' => 'TEXT'] - ]; - $forge->addColumn('table_name', $fields); - // Executes: ALTER TABLE table_name ADD preferences TEXT + $fields = [ + 'preferences' => ['type' => 'TEXT'] + ]; + $forge->addColumn('table_name', $fields); + // Executes: ALTER TABLE table_name ADD preferences TEXT If you are using MySQL or CUBIRD, then you can take advantage of their AFTER and FIRST clauses to position the new column. Examples:: - // Will place the new column after the `another_field` column: - $fields = [ - 'preferences' => ['type' => 'TEXT', 'after' => 'another_field'] - ]; + // Will place the new column after the `another_field` column: + $fields = [ + 'preferences' => ['type' => 'TEXT', 'after' => 'another_field'] + ]; - // Will place the new column at the start of the table definition: - $fields = [ - 'preferences' => ['type' => 'TEXT', 'first' => true] - ]; + // Will place the new column at the start of the table definition: + $fields = [ + 'preferences' => ['type' => 'TEXT', 'first' => true] + ]; Dropping Columns From a Table ============================== @@ -359,7 +359,7 @@ Used to remove a column from a table. :: - $forge->dropColumn('table_name', 'column_to_drop'); // to drop one single column + $forge->dropColumn('table_name', 'column_to_drop'); // to drop one single column Used to remove multiple columns from a table. @@ -379,14 +379,14 @@ change the name, you can add a "name" key into the field defining array. :: - $fields = [ - 'old_name' => [ - 'name' => 'new_name', - 'type' => 'TEXT', - ], - ]; - $forge->modifyColumn('table_name', $fields); - // gives ALTER TABLE table_name CHANGE old_name new_name TEXT + $fields = [ + 'old_name' => [ + 'name' => 'new_name', + 'type' => 'TEXT', + ], + ]; + $forge->modifyColumn('table_name', $fields); + // gives ALTER TABLE table_name CHANGE old_name new_name TEXT *************** Class Reference @@ -394,108 +394,120 @@ Class Reference .. php:class:: CodeIgniter\\Database\\Forge - .. php:method:: addColumn($table[, $field = []]) + .. php:method:: addColumn($table[, $field = []]) - :param string $table: Table name to add the column to - :param array $field: Column definition(s) - :returns: true on success, false on failure - :rtype: bool + :param string $table: Table name to add the column to + :param array $field: Column definition(s) + :returns: true on success, false on failure + :rtype: bool - Adds a column to a table. Usage: See `Adding a Column to a Table`_. + Adds a column to a table. Usage: See `Adding a Column to a Table`_. - .. php:method:: addField($field) + .. php:method:: addField($field) - :param array $field: Field definition to add - :returns: \CodeIgniter\Database\Forge instance (method chaining) - :rtype: \CodeIgniter\Database\Forge + :param array $field: Field definition to add + :returns: \CodeIgniter\Database\Forge instance (method chaining) + :rtype: \CodeIgniter\Database\Forge Adds a field to the set that will be used to create a table. Usage: See `Adding fields`_. - .. php:method:: addKey($key[, $primary = false[, $unique = false]]) + .. php:method:: addForeignKey($fieldName, $tableName, $tableField[, $onUpdate = '', $onDelete = '']) - :param mixed $key: Name of a key field or an array of fields - :param bool $primary: Set to true if it should be a primary key or a regular one - :param bool $unique: Set to true if it should be a unique key or a regular one - :returns: \CodeIgniter\Database\Forge instance (method chaining) - :rtype: \CodeIgniter\Database\Forge + :param string|string[] $fieldName: Name of a key field or an array of fields + :param string $tableName: Name of a parent table + :param string|string[] $tableField: Name of a parent table field or an array of fields + :param string $onUpdate: Desired action for the “on update” + :param string $onDelete: Desired action for the “on delete” + :returns: \CodeIgniter\Database\Forge instance (method chaining) + :rtype: \CodeIgniter\Database\Forge - Adds a key to the set that will be used to create a table. Usage: See `Adding Keys`_. + Adds a foreign key to the set that will be used to create a table. Usage: See `Adding Foreign Keys`_. - .. php:method:: addPrimaryKey($key) + .. php:method:: addKey($key[, $primary = false[, $unique = false]]) - :param mixed $key: Name of a key field or an array of fields - :returns: \CodeIgniter\Database\Forge instance (method chaining) - :rtype: \CodeIgniter\Database\Forge + :param mixed $key: Name of a key field or an array of fields + :param bool $primary: Set to true if it should be a primary key or a regular one + :param bool $unique: Set to true if it should be a unique key or a regular one + :returns: \CodeIgniter\Database\Forge instance (method chaining) + :rtype: \CodeIgniter\Database\Forge - Adds a primary key to the set that will be used to create a table. Usage: See `Adding Keys`_. + Adds a key to the set that will be used to create a table. Usage: See `Adding Keys`_. - .. php:method:: addUniqueKey($key) + .. php:method:: addPrimaryKey($key) - :param mixed $key: Name of a key field or an array of fields - :returns: \CodeIgniter\Database\Forge instance (method chaining) - :rtype: \CodeIgniter\Database\Forge + :param mixed $key: Name of a key field or an array of fields + :returns: \CodeIgniter\Database\Forge instance (method chaining) + :rtype: \CodeIgniter\Database\Forge - Adds a unique key to the set that will be used to create a table. Usage: See `Adding Keys`_. + Adds a primary key to the set that will be used to create a table. Usage: See `Adding Keys`_. - .. php:method:: createDatabase($dbName[, $ifNotExists = false]) + .. php:method:: addUniqueKey($key) - :param string $db_name: Name of the database to create - :param string $ifNotExists: Set to true to add an 'IF NOT EXISTS' clause or check if database exists - :returns: true on success, false on failure - :rtype: bool + :param mixed $key: Name of a key field or an array of fields + :returns: \CodeIgniter\Database\Forge instance (method chaining) + :rtype: \CodeIgniter\Database\Forge - Creates a new database. Usage: See `Creating and Dropping Databases`_. + Adds a unique key to the set that will be used to create a table. Usage: See `Adding Keys`_. - .. php:method:: createTable($table[, $if_not_exists = false[, array $attributes = []]]) + .. php:method:: createDatabase($dbName[, $ifNotExists = false]) - :param string $table: Name of the table to create - :param string $if_not_exists: Set to true to add an 'IF NOT EXISTS' clause - :param string $attributes: An associative array of table attributes - :returns: Query object on success, false on failure - :rtype: mixed + :param string $db_name: Name of the database to create + :param string $ifNotExists: Set to true to add an 'IF NOT EXISTS' clause or check if database exists + :returns: true on success, false on failure + :rtype: bool - Creates a new table. Usage: See `Creating a table`_. + Creates a new database. Usage: See `Creating and Dropping Databases`_. - .. php:method:: dropColumn($table, $column_name) + .. php:method:: createTable($table[, $if_not_exists = false[, array $attributes = []]]) - :param string $table: Table name - :param mixed $column_names: Comma-delimited string or an array of column names - :returns: true on success, false on failure - :rtype: bool + :param string $table: Name of the table to create + :param string $if_not_exists: Set to true to add an 'IF NOT EXISTS' clause + :param string $attributes: An associative array of table attributes + :returns: Query object on success, false on failure + :rtype: mixed - Drops single or multiple columns from a table. Usage: See `Dropping Columns From a Table`_. + Creates a new table. Usage: See `Creating a table`_. - .. php:method:: dropDatabase($dbName) + .. php:method:: dropColumn($table, $column_name) - :param string $dbName: Name of the database to drop - :returns: true on success, false on failure - :rtype: bool + :param string $table: Table name + :param mixed $column_names: Comma-delimited string or an array of column names + :returns: true on success, false on failure + :rtype: bool - Drops a database. Usage: See `Creating and Dropping Databases`_. + Drops single or multiple columns from a table. Usage: See `Dropping Columns From a Table`_. - .. php:method:: dropTable($table_name[, $if_exists = false]) + .. php:method:: dropDatabase($dbName) - :param string $table: Name of the table to drop - :param string $if_exists: Set to true to add an 'IF EXISTS' clause - :returns: true on success, false on failure - :rtype: bool + :param string $dbName: Name of the database to drop + :returns: true on success, false on failure + :rtype: bool - Drops a table. Usage: See `Dropping a table`_. + Drops a database. Usage: See `Creating and Dropping Databases`_. - .. php:method:: modifyColumn($table, $field) + .. php:method:: dropTable($table_name[, $if_exists = false]) - :param string $table: Table name - :param array $field: Column definition(s) - :returns: true on success, false on failure - :rtype: bool + :param string $table: Name of the table to drop + :param string $if_exists: Set to true to add an 'IF EXISTS' clause + :returns: true on success, false on failure + :rtype: bool - Modifies a table column. Usage: See `Modifying a Column in a Table`_. + Drops a table. Usage: See `Dropping a table`_. - .. php:method:: renameTable($table_name, $new_table_name) + .. php:method:: modifyColumn($table, $field) - :param string $table: Current of the table - :param string $new_table_name: New name of the table - :returns: Query object on success, false on failure - :rtype: mixed + :param string $table: Table name + :param array $field: Column definition(s) + :returns: true on success, false on failure + :rtype: bool - Renames a table. Usage: See `Renaming a table`_. + Modifies a table column. Usage: See `Modifying a Column in a Table`_. + + .. php:method:: renameTable($table_name, $new_table_name) + + :param string $table: Current of the table + :param string $new_table_name: New name of the table + :returns: Query object on success, false on failure + :rtype: mixed + + Renames a table. Usage: See `Renaming a table`_. From 1b55d7bc98e334578da7f707ce4676689423bbed Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 22:12:13 +0900 Subject: [PATCH 117/490] docs: add about upgrading migration table --- user_guide_src/source/installation/upgrade_migrations.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/user_guide_src/source/installation/upgrade_migrations.rst b/user_guide_src/source/installation/upgrade_migrations.rst index 13a0082ff983..66bfcbaabc1d 100644 --- a/user_guide_src/source/installation/upgrade_migrations.rst +++ b/user_guide_src/source/installation/upgrade_migrations.rst @@ -15,6 +15,8 @@ What has been changed ===================== - First of all, the sequential naming (``001_create_users``, ``002_create_posts``) of migrations is not longer supported. Version 4 of CodeIgniter only supports the timestamp scheme (``20121031100537_create_users``, ``20121031500638_create_posts``) . If you have used sequential naming you have to rename each migration file. +- The migration table definition was changed. If you upgrade from CI3 to CI4 and use the same database, + You need to upgrade the migration table definition and its data. - The migration procedure has been also changed. You can now migrate the database with a simple CLI command:: > php spark migrate @@ -36,6 +38,12 @@ Upgrade Guide - ``$this->dbforge->drop_table`` to ``$this->forge->addTable`` 8. (optional) You can change the array syntax from ``array(...)`` to ``[...]`` +9. Upgrade the migration table, if you use the same database. + + - **(development)** Run the CI4 migration in the development environment or so with brand new database, to create the new migration table. + - **(development)** Export the migration table. + - **(production)** Drop (or rename) the existing CI3 migration table. + - **(production)** Import the new migration table and the data. Code Example ============ From 04a31f00f6b293bf82587179cab9bd93e54ebb86 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Sep 2021 22:13:09 +0900 Subject: [PATCH 118/490] docs: rename v4.2.0 to v4.1.5 --- user_guide_src/source/changelogs/index.rst | 2 +- user_guide_src/source/changelogs/{v4.2.0.rst => v4.1.5.rst} | 4 ++-- .../source/installation/{upgrade_420.rst => upgrade_415.rst} | 2 +- user_guide_src/source/installation/upgrading.rst | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename user_guide_src/source/changelogs/{v4.2.0.rst => v4.1.5.rst} (88%) rename user_guide_src/source/installation/{upgrade_420.rst => upgrade_415.rst} (97%) diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index a997a57f0c3f..2ec12f649150 100644 --- a/user_guide_src/source/changelogs/index.rst +++ b/user_guide_src/source/changelogs/index.rst @@ -12,7 +12,7 @@ See all the changes. .. toctree:: :titlesonly: - v4.2.0 + v4.1.5 v4.1.4 v4.1.3 v4.1.2 diff --git a/user_guide_src/source/changelogs/v4.2.0.rst b/user_guide_src/source/changelogs/v4.1.5.rst similarity index 88% rename from user_guide_src/source/changelogs/v4.2.0.rst rename to user_guide_src/source/changelogs/v4.1.5.rst index 22a94202a69f..b57f3b79a85a 100644 --- a/user_guide_src/source/changelogs/v4.2.0.rst +++ b/user_guide_src/source/changelogs/v4.1.5.rst @@ -1,9 +1,9 @@ -Version 4.2.0 +Version 4.1.5 ============= Release Date: Not released -**4.2.0 release of CodeIgniter4** +**4.1.5 release of CodeIgniter4** Enhancements: diff --git a/user_guide_src/source/installation/upgrade_420.rst b/user_guide_src/source/installation/upgrade_415.rst similarity index 97% rename from user_guide_src/source/installation/upgrade_420.rst rename to user_guide_src/source/installation/upgrade_415.rst index cfbcbdbf1aeb..bb075023e7bc 100644 --- a/user_guide_src/source/installation/upgrade_420.rst +++ b/user_guide_src/source/installation/upgrade_415.rst @@ -1,5 +1,5 @@ ############################# -Upgrading from 4.1.4 to 4.2.0 +Upgrading from 4.1.4 to 4.1.5 ############################# **Changes for set() method in BaseBuilder and Model class** diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index b6ef428e8b47..0264b6eaa3bb 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -8,7 +8,7 @@ upgrading from. .. toctree:: :titlesonly: - Upgrading from 4.1.4 to 4.2.0 + Upgrading from 4.1.4 to 4.1.5 Upgrading from 4.1.3 to 4.1.4 Upgrading from 4.1.2 to 4.1.3 Upgrading from 4.1.1 to 4.1.2 From a7d7de65c2747dcc20939aaef8fded199c22605c Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 13 Sep 2021 13:48:08 +0700 Subject: [PATCH 119/490] [Rector] Clean up rector skip config --- rector.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/rector.php b/rector.php index 054ed3ea09ad..931b89304416 100644 --- a/rector.php +++ b/rector.php @@ -28,7 +28,6 @@ use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; use Rector\Core\Configuration\Option; use Rector\Core\ValueObject\PhpVersion; -use Rector\DeadCode\Rector\Cast\RecastingRemovalRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector; use Rector\DeadCode\Rector\If_\UnwrapFutureCompatibleIfPhpVersionRector; @@ -91,11 +90,6 @@ __DIR__ . '/system/CodeIgniter.php', ], - // casted to Entity via EntityTest->getCastEntity() - RecastingRemovalRector::class => [ - __DIR__ . '/tests/system/Entity/EntityTest.php', - ], - // session handlers have the gc() method with underscored parameter `$max_lifetime` UnderscoreToCamelCaseVariableNameRector::class => [ __DIR__ . '/system/Session/Handlers', From 419eb39a63cad50aee155783399bd0350c1efeae Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 13 Sep 2021 15:58:28 +0900 Subject: [PATCH 120/490] docs: fix query_builder RST format --- .../source/database/query_builder.rst | 106 +++++++----------- 1 file changed, 40 insertions(+), 66 deletions(-) diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 3494325e5084..45380416fdac 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -259,9 +259,7 @@ methods: Notice that the equal sign is added for you. If you use multiple function calls they will be chained together with - AND between them: - - :: + AND between them:: $builder->where('name', $name); $builder->where('title', $title); @@ -271,9 +269,7 @@ methods: #. **Custom key/value method:** You can include an operator in the first parameter in order to - control the comparison: - - :: + control the comparison:: $builder->where('name !=', $name); $builder->where('id <', $id); @@ -287,33 +283,30 @@ methods: $builder->where($array); // Produces: WHERE name = 'Joe' AND title = 'boss' AND status = 'active' - You can include your own operators using this method as well: - - :: + You can include your own operators using this method as well:: $array = ['name !=' => $name, 'id <' => $id, 'date >' => $date]; $builder->where($array); #. **Custom string:** - You can write your own clauses manually:: + You can write your own clauses manually:: $where = "name='Joe' AND status='boss' OR status='active'"; $builder->where($where); If you are using user-supplied data within the string, you MUST escape the data manually. Failure to do so could result in SQL injections. -:: + + :: $name = $builder->db->escape('Joe'); $where = "name={$name} AND status='boss' OR status='active'"; $builder->where($where); - #. **Subqueries:** - You can use an anonymous function to create a subquery. - :: + You can use an anonymous function to create a subquery:: $builder->where('advance_amount <', function (BaseBuilder $builder) { return $builder->select('MAX(advance_amount)', false)->from('orders')->where('id >', 2); @@ -323,48 +316,38 @@ methods: **$builder->orWhere()** This function is identical to the one above, except that multiple -instances are joined by OR - - :: +instances are joined by OR:: - $builder->where('name !=', $name); - $builder->orWhere('id >', $id); - // Produces: WHERE name != 'Joe' OR id > 50 + $builder->where('name !=', $name); + $builder->orWhere('id >', $id); + // Produces: WHERE name != 'Joe' OR id > 50 **$builder->whereIn()** Generates a WHERE field IN ('item', 'item') SQL query joined with AND if -appropriate - - :: +appropriate:: - $names = ['Frank', 'Todd', 'James']; - $builder->whereIn('username', $names); - // Produces: WHERE username IN ('Frank', 'Todd', 'James') - -You can use subqueries instead of an array of values. + $names = ['Frank', 'Todd', 'James']; + $builder->whereIn('username', $names); + // Produces: WHERE username IN ('Frank', 'Todd', 'James') - :: +You can use subqueries instead of an array of values:: - $builder->whereIn('id', function (BaseBuilder $builder) { - return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); - }); - // Produces: WHERE "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) + $builder->whereIn('id', function (BaseBuilder $builder) { + return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); + }); + // Produces: WHERE "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) **$builder->orWhereIn()** Generates a ``WHERE field IN ('item', 'item')`` SQL query joined with OR if -appropriate - - :: +appropriate:: $names = ['Frank', 'Todd', 'James']; $builder->orWhereIn('username', $names); // Produces: OR username IN ('Frank', 'Todd', 'James') -You can use subqueries instead of an array of values. - - :: +You can use subqueries instead of an array of values:: $builder->orWhereIn('id', function (BaseBuilder $builder) { return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); @@ -375,45 +358,36 @@ You can use subqueries instead of an array of values. **$builder->whereNotIn()** Generates a WHERE field NOT IN ('item', 'item') SQL query joined with -AND if appropriate +AND if appropriate:: - :: - - $names = ['Frank', 'Todd', 'James']; - $builder->whereNotIn('username', $names); - // Produces: WHERE username NOT IN ('Frank', 'Todd', 'James') + $names = ['Frank', 'Todd', 'James']; + $builder->whereNotIn('username', $names); + // Produces: WHERE username NOT IN ('Frank', 'Todd', 'James') -You can use subqueries instead of an array of values. +You can use subqueries instead of an array of values:: - :: - - $builder->whereNotIn('id', function (BaseBuilder $builder) { - return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); - }); - - // Produces: WHERE "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) + $builder->whereNotIn('id', function (BaseBuilder $builder) { + return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); + }); + // Produces: WHERE "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) **$builder->orWhereNotIn()** Generates a ``WHERE field NOT IN ('item', 'item')`` SQL query joined with OR -if appropriate +if appropriate:: - :: + $names = ['Frank', 'Todd', 'James']; + $builder->orWhereNotIn('username', $names); + // Produces: OR username NOT IN ('Frank', 'Todd', 'James') - $names = ['Frank', 'Todd', 'James']; - $builder->orWhereNotIn('username', $names); - // Produces: OR username NOT IN ('Frank', 'Todd', 'James') +You can use subqueries instead of an array of values:: -You can use subqueries instead of an array of values. - - :: - - $builder->orWhereNotIn('id', function (BaseBuilder $builder) { - return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); - }); + $builder->orWhereNotIn('id', function (BaseBuilder $builder) { + return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); + }); - // Produces: OR "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) + // Produces: OR "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) ************************ Looking for Similar Data From e4194f4273cc9550df43dd8f3691b134171192d0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 13 Sep 2021 15:58:54 +0900 Subject: [PATCH 121/490] docs: fix controller classnames in sample code --- .../source/installation/upgrade_routing.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_routing.rst b/user_guide_src/source/installation/upgrade_routing.rst index 4b30b24f7f2c..7243d41345db 100644 --- a/user_guide_src/source/installation/upgrade_routing.rst +++ b/user_guide_src/source/installation/upgrade_routing.rst @@ -64,12 +64,12 @@ Path: ``app/Config/Routes.php``:: ... - $routes->add('posts', 'posts::index'); - $routes->add('teams/create', 'teams::create'); - $routes->add('teams/edit/(:any)', 'teams::edit/$1'); - - $routes->add('posts/create', 'posts::create'); - $routes->add('posts/edit/(:any)', 'posts::edit/$1'); - $routes->add('drivers/create', 'drivers::create'); - $routes->add('drivers/edit/(:any)', 'drivers::edit/$1'); - $routes->add('posts/(:any)', 'posts::view/$1'); + $routes->add('posts', 'Posts::index'); + $routes->add('teams/create', 'Teams::create'); + $routes->add('teams/edit/(:any)', 'Teams::edit/$1'); + + $routes->add('posts/create', 'Posts::create'); + $routes->add('posts/edit/(:any)', 'Posts::edit/$1'); + $routes->add('drivers/create', 'Drivers::create'); + $routes->add('drivers/edit/(:any)', 'Drivers::edit/$1'); + $routes->add('posts/(:any)', 'Posts::view/$1'); From 6b5b8dec061541cefb35d760a15fbd53147eba3f Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 13 Sep 2021 15:59:45 +0900 Subject: [PATCH 122/490] docs: small fixes for configuration --- .../source/general/configuration.rst | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/user_guide_src/source/general/configuration.rst b/user_guide_src/source/general/configuration.rst index ee1486d91056..5f43477bbaaf 100644 --- a/user_guide_src/source/general/configuration.rst +++ b/user_guide_src/source/general/configuration.rst @@ -124,9 +124,9 @@ To save on typing, you can reuse variables that you've already specified in the variable name within ``${...}`` :: - BASE_DIR="/var/webroot/project-root" - CACHE_DIR="${BASE_DIR}/cache" - TMP_DIR="${BASE_DIR}/tmp" + BASE_DIR="/var/webroot/project-root" + CACHE_DIR="${BASE_DIR}/cache" + TMP_DIR="${BASE_DIR}/tmp" Namespaced Variables ==================== @@ -262,17 +262,17 @@ wish to supply an additional template to ``Pager`` without overwriting whatever already configured. In **src/Config/Registrar.php** there would be a ``Registrar`` class with the single ``Pager()`` method (note the case-sensitivity):: - class Registrar - { - public static function Pager(): array - { - return [ - 'templates' => [ - 'module_pager' => 'MyModule\Views\Pager', - ], - ]; - } - } + class Registrar + { + public static function Pager(): array + { + return [ + 'templates' => [ + 'module_pager' => 'MyModule\Views\Pager', + ], + ]; + } + } Registrar methods must always return an array, with keys corresponding to the properties of the target config file. Existing values are merged, and Registrar properties have @@ -337,4 +337,3 @@ by treating ``RegionalSales`` as a "registrar". The resulting configuration prop $target = 45; $campaign = "Winter Wonderland"; - From 7823fe8d281630cb8230d3c9d024a7446bb2a136 Mon Sep 17 00:00:00 2001 From: MGatner Date: Tue, 14 Sep 2021 23:05:23 +0000 Subject: [PATCH 123/490] Ignore unroutable responses --- system/CodeIgniter.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index aee11790f406..1bcb12a627b9 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -19,6 +19,7 @@ use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\DownloadResponse; use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; @@ -929,6 +930,11 @@ public function storePreviousURL($uri) return; } + // Ignore unroutable responses + if ($this->response instanceof DownloadResponse || $this->response instanceof RedirectResponse) { + return; + } + // This is mainly needed during testing... if (is_string($uri)) { $uri = new URI($uri); From 8e1b2805ce6e59a6bfc430a2cada8f7e19b787f6 Mon Sep 17 00:00:00 2001 From: MGatner Date: Wed, 15 Sep 2021 01:47:19 +0000 Subject: [PATCH 124/490] Add tests --- system/CodeIgniter.php | 4 ++-- tests/system/CodeIgniterTest.php | 23 ++++++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 1bcb12a627b9..1ac65c4663cd 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -922,8 +922,8 @@ protected function gatherOutput(?Cache $cacheConfig = null, $returned = null) public function storePreviousURL($uri) { // Ignore CLI requests - if (is_cli()) { - return; + if (is_cli() && ENVIRONMENT !== 'testing') { + return; // @codeCoverageIgnore } // Ignore AJAX requests if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX()) { diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index af0488f3c7f5..0a3ad80921d7 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -345,7 +345,24 @@ public function testRunRedirectionWithHTTPCode303() $this->assertSame(303, $response->getStatusCode()); } - public function testRunRedirectionWithHTTPCode301() + public function testStoresPreviousURL() + { + $_SERVER['argv'] = ['index.php', '/']; + $_SERVER['argc'] = 2; + + // Inject mock router. + $router = Services::router(null, Services::request(), false); + Services::injectMock('router', $router); + + ob_start(); + $this->codeigniter->useSafeOutput(true)->run(); + ob_get_clean(); + + $this->assertArrayHasKey('_ci_previous_url', $_SESSION); + $this->assertSame('http://example.com/index.php', $_SESSION['_ci_previous_url']); + } + + public function testNotStoresPreviousURL() { $_SERVER['argv'] = ['index.php', 'example']; $_SERVER['argc'] = 2; @@ -364,8 +381,8 @@ public function testRunRedirectionWithHTTPCode301() ob_start(); $this->codeigniter->useSafeOutput(true)->run(); ob_get_clean(); - $response = $this->getPrivateProperty($this->codeigniter, 'response'); - $this->assertSame(301, $response->getStatusCode()); + + $this->assertArrayNotHasKey('_ci_previous_url', $_SESSION); } /** From 9b202fcc4e3f4ba2c0d47d258a72d3fd4c13ec83 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Sep 2021 12:08:27 +0900 Subject: [PATCH 125/490] docs: fix indentation --- user_guide_src/source/dbmgmt/migration.rst | 227 +++++++++++---------- 1 file changed, 114 insertions(+), 113 deletions(-) diff --git a/user_guide_src/source/dbmgmt/migration.rst b/user_guide_src/source/dbmgmt/migration.rst index 88b2a2c1e106..027c12d3dc70 100644 --- a/user_guide_src/source/dbmgmt/migration.rst +++ b/user_guide_src/source/dbmgmt/migration.rst @@ -43,41 +43,41 @@ migrations go in the **app/Database/Migrations/** directory and have names such as *20121031100537_add_blog.php*. :: - forge->addField([ - 'blog_id' => [ - 'type' => 'INT', - 'constraint' => 5, - 'unsigned' => true, - 'auto_increment' => true, - ], - 'blog_title' => [ - 'type' => 'VARCHAR', - 'constraint' => '100', - ], - 'blog_description' => [ - 'type' => 'TEXT', - 'null' => true, - ], - ]); - $this->forge->addKey('blog_id', true); - $this->forge->createTable('blog'); - } - - public function down() - { - $this->forge->dropTable('blog'); - } - } + forge->addField([ + 'blog_id' => [ + 'type' => 'INT', + 'constraint' => 5, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'blog_title' => [ + 'type' => 'VARCHAR', + 'constraint' => '100', + ], + 'blog_description' => [ + 'type' => 'TEXT', + 'null' => true, + ], + ]); + $this->forge->addKey('blog_id', true); + $this->forge->createTable('blog'); + } + + public function down() + { + $this->forge->dropTable('blog'); + } + } The database connection and the database Forge class are both available to you through ``$this->db`` and ``$this->forge``, respectively. @@ -94,14 +94,14 @@ To temporarily bypass the foreign key checks while running migrations, use the ` :: - public function up() - { - $this->db->disableForeignKeyChecks() + public function up() + { + $this->db->disableForeignKeyChecks() - // Migration rules would go here.. + // Migration rules would go here.. - $this->db->enableForeignKeyChecks(); - } + $this->db->enableForeignKeyChecks(); + } Database Groups =============== @@ -114,26 +114,26 @@ another database is used for mission critical data. You can ensure that migratio against the proper group by setting the ``$DBGroup`` property on your migration. This name must match the name of the database group exactly:: - APPPATH, - 'MyCompany' => ROOTPATH . 'MyCompany', - ]; + $psr4 = [ + 'App' => APPPATH, + 'MyCompany' => ROOTPATH . 'MyCompany', + ]; This will look for any migrations located at both **APPPATH/Database/Migrations** and **ROOTPATH/MyCompany/Database/Migrations**. This makes it simple to include migrations in your @@ -164,23 +164,23 @@ Usage Example In this example some simple code is placed in **app/Controllers/Migrate.php** to update the schema:: - latest(); - } catch (\Throwable $e) { - // Do something with the error here... - } - } - } + try { + $migrate->latest(); + } catch (\Throwable $e) { + // Do something with the error here... + } + } + } ******************* Command-Line Tools @@ -284,62 +284,63 @@ Class Reference .. php:class:: CodeIgniter\\Database\\MigrationRunner - .. php:method:: findMigrations() + .. php:method:: findMigrations() - :returns: An array of migration files - :rtype: array + :returns: An array of migration files + :rtype: array - An array of migration filenames are returned that are found in the **path** property. + An array of migration filenames are returned that are found in the **path** property. - .. php:method:: latest($group) + .. php:method:: latest($group) - :param mixed $group: database group name, if null default database group will be used. - :returns: ``true`` on success, ``false`` on failure - :rtype: bool + :param mixed $group: database group name, if null default database group will be used. + :returns: ``true`` on success, ``false`` on failure + :rtype: bool - This locates migrations for a namespace (or all namespaces), determines which migrations - have not yet been run, and runs them in order of their version (namespaces intermingled). + This locates migrations for a namespace (or all namespaces), determines which migrations + have not yet been run, and runs them in order of their version (namespaces intermingled). - .. php:method:: regress($batch, $group) + .. php:method:: regress($batch, $group) - :param mixed $batch: previous batch to migrate down to; 1+ specifies the batch, 0 reverts all, negative refers to the relative batch (e.g., -3 means "three batches back") - :param mixed $group: database group name, if null default database group will be used. - :returns: ``true`` on success, ``false`` on failure or no migrations are found - :rtype: bool + :param mixed $batch: previous batch to migrate down to; 1+ specifies the batch, 0 reverts all, negative refers to the relative batch (e.g., -3 means "three batches back") + :param mixed $group: database group name, if null default database group will be used. + :returns: ``true`` on success, ``false`` on failure or no migrations are found + :rtype: bool - Regress can be used to roll back changes to a previous state, batch by batch. - :: + Regress can be used to roll back changes to a previous state, batch by batch. + :: - $migration->regress(5); - $migration->regress(-1); + $migration->regress(5); + $migration->regress(-1); - .. php:method:: force($path, $namespace, $group) + .. php:method:: force($path, $namespace, $group) - :param mixed $path: path to a valid migration file. - :param mixed $namespace: namespace of the provided migration. - :param mixed $group: database group name, if null default database group will be used. - :returns: ``true`` on success, ``false`` on failure - :rtype: bool + :param mixed $path: path to a valid migration file. + :param mixed $namespace: namespace of the provided migration. + :param mixed $group: database group name, if null default database group will be used. + :returns: ``true`` on success, ``false`` on failure + :rtype: bool - This forces a single file to migrate regardless of order or batches. Method "up" or "down" is detected based on whether it has already been migrated. + This forces a single file to migrate regardless of order or batches. Method "up" or "down" is detected based on whether it has already been migrated. - .. note:: This method is recommended only for testing and could cause data consistency issues. + .. note:: This method is recommended only for testing and could cause data consistency issues. - .. php:method:: setNamespace($namespace) + .. php:method:: setNamespace($namespace) - :param string $namespace: application namespace. - :returns: The current MigrationRunner instance - :rtype: CodeIgniter\\Database\\MigrationRunner + :param string $namespace: application namespace. + :returns: The current MigrationRunner instance + :rtype: CodeIgniter\\Database\\MigrationRunner - Sets the namespace the library should look for migration files:: + Sets the namespace the library should look for migration files:: - $migration->setNamespace($namespace)->latest(); - .. php:method:: setGroup($group) + $migration->setNamespace($namespace)->latest(); - :param string $group: database group name. - :returns: The current MigrationRunner instance - :rtype: CodeIgniter\\Database\\MigrationRunner + .. php:method:: setGroup($group) - Sets the group the library should look for migration files:: + :param string $group: database group name. + :returns: The current MigrationRunner instance + :rtype: CodeIgniter\\Database\\MigrationRunner - $migration->setGroup($group)->latest(); + Sets the group the library should look for migration files:: + + $migration->setGroup($group)->latest(); From 094b95a62fdd627b761268053e2c93444d89a68e Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Sep 2021 12:09:00 +0900 Subject: [PATCH 126/490] docs: add new line at end of file --- user_guide_src/source/database/call_function.rst | 2 +- user_guide_src/source/database/index.rst | 2 +- user_guide_src/source/general/ajax.rst | 2 +- user_guide_src/source/helpers/number_helper.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/database/call_function.rst b/user_guide_src/source/database/call_function.rst index 6c0453146cd6..9617c2361df0 100644 --- a/user_guide_src/source/database/call_function.rst +++ b/user_guide_src/source/database/call_function.rst @@ -40,4 +40,4 @@ The result ID can be accessed from within your result object, like this:: $query = $db->query("SOME QUERY"); - $query->resultID; \ No newline at end of file + $query->resultID; diff --git a/user_guide_src/source/database/index.rst b/user_guide_src/source/database/index.rst index 62002afd00a8..73cd1a4ecff9 100644 --- a/user_guide_src/source/database/index.rst +++ b/user_guide_src/source/database/index.rst @@ -20,4 +20,4 @@ patterns. The database functions offer clear, simple syntax. Getting MetaData Custom Function Calls Database Events - Database Utilities \ No newline at end of file + Database Utilities diff --git a/user_guide_src/source/general/ajax.rst b/user_guide_src/source/general/ajax.rst index 6324930b199b..710406b41272 100644 --- a/user_guide_src/source/general/ajax.rst +++ b/user_guide_src/source/general/ajax.rst @@ -50,4 +50,4 @@ React .. code-block:: javascript - axios.get("your url", {headers: {'Content-Type': 'application/json'}}) \ No newline at end of file + axios.get("your url", {headers: {'Content-Type': 'application/json'}}) diff --git a/user_guide_src/source/helpers/number_helper.rst b/user_guide_src/source/helpers/number_helper.rst index 2c6c5045ac77..ae5f8238b156 100644 --- a/user_guide_src/source/helpers/number_helper.rst +++ b/user_guide_src/source/helpers/number_helper.rst @@ -115,4 +115,4 @@ The following functions are available: echo number_to_roman(2534); // Returns MMDXXXIV This function only handles numbers in the range 1 through 3999. - It will return null for any value outside that range . \ No newline at end of file + It will return null for any value outside that range. From 7610085940b2831d9d63bc62c26505a61b51b04b Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Sep 2021 12:09:35 +0900 Subject: [PATCH 127/490] docs: replace tab with space --- user_guide_src/source/testing/controllers.rst | 46 +++++++++---------- user_guide_src/source/testing/feature.rst | 6 +-- user_guide_src/source/testing/overview.rst | 26 +++++------ user_guide_src/source/testing/response.rst | 8 ++-- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/user_guide_src/source/testing/controllers.rst b/user_guide_src/source/testing/controllers.rst index 464a06c5fbcd..4333ad26ec0e 100644 --- a/user_guide_src/source/testing/controllers.rst +++ b/user_guide_src/source/testing/controllers.rst @@ -209,13 +209,13 @@ on an unfiltered route you could add it to the Config:: { use FilterTestTrait; - protected function testFilterFailsOnAdminRoute() - { - $this->filtersConfig->globals['before'] = ['admin-only-filter']; + protected function testFilterFailsOnAdminRoute() + { + $this->filtersConfig->globals['before'] = ['admin-only-filter']; - $this->assertHasFilters('unfiltered/route', 'before'); - } - ... + $this->assertHasFilters('unfiltered/route', 'before'); + } + ... Checking Routes --------------- @@ -227,14 +227,14 @@ a large performance advantage over Controller and HTTP Testing. .. php:function:: getFiltersForRoute($route, $position) - :param string $route: The URI to check - :param string $position: The filter method to check, "before" or "after" - :returns: Aliases for each filter that would have run - :rtype: string[] + :param string $route: The URI to check + :param string $position: The filter method to check, "before" or "after" + :returns: Aliases for each filter that would have run + :rtype: string[] Usage example:: - $result = $this->getFiltersForRoute('/', 'after'); // ['toolbar'] + $result = $this->getFiltersForRoute('/', 'after'); // ['toolbar'] Calling Filter Methods ---------------------- @@ -245,22 +245,22 @@ method using these properties to test your Filter code safely and check the resu .. php:function:: getFilterCaller($filter, $position) - :param FilterInterface|string $filter: The filter instance, class, or alias - :param string $position: The filter method to run, "before" or "after" - :returns: A callable method to run the simulated Filter event - :rtype: Closure + :param FilterInterface|string $filter: The filter instance, class, or alias + :param string $position: The filter method to run, "before" or "after" + :returns: A callable method to run the simulated Filter event + :rtype: Closure Usage example:: - protected function testUnauthorizedAccessRedirects() - { - $caller = $this->getFilterCaller('permission', 'before'); - $result = $caller('MayEditWidgets'); + protected function testUnauthorizedAccessRedirects() + { + $caller = $this->getFilterCaller('permission', 'before'); + $result = $caller('MayEditWidgets'); + + $this->assertInstanceOf('CodeIgniter\HTTP\RedirectResponse', $result); + } - $this->assertInstanceOf('CodeIgniter\HTTP\RedirectResponse', $result); - } - - Notice how the ``Closure`` can take input parameters which are passed to your filter method. + Notice how the ``Closure`` can take input parameters which are passed to your filter method. Assertions ---------- diff --git a/user_guide_src/source/testing/feature.rst b/user_guide_src/source/testing/feature.rst index 6ec786c67079..c48c24b221c5 100644 --- a/user_guide_src/source/testing/feature.rst +++ b/user_guide_src/source/testing/feature.rst @@ -29,20 +29,20 @@ are called if you implement your own methods. class TestFoo extends FeatureTestCase { - use DatabaseTestTrait, FeatureTestTrait; + use DatabaseTestTrait, FeatureTestTrait; protected function setUp(): void { parent::setUp(); - $this->myClassMethod(); + $this->myClassMethod(); } protected function tearDown(): void { parent::tearDown(); - $this->anotherClassMethod(); + $this->anotherClassMethod(); } } diff --git a/user_guide_src/source/testing/overview.rst b/user_guide_src/source/testing/overview.rst index 28e99b9b4bbc..f237acc03412 100644 --- a/user_guide_src/source/testing/overview.rst +++ b/user_guide_src/source/testing/overview.rst @@ -159,19 +159,19 @@ to run named for the trait itself. For example, if you needed to add authenticat of your test cases you could create an authentication trait with a set up method to fake a logged in user:: - trait AuthTrait - { - protected setUpAuthTrait() - { - $user = $this->createFakeUser(); - $this->logInUser($user); - } - ... - - class AuthenticationFeatureTest - { - use AuthTrait; - ... + trait AuthTrait + { + protected setUpAuthTrait() + { + $user = $this->createFakeUser(); + $this->logInUser($user); + } + ... + + class AuthenticationFeatureTest + { + use AuthTrait; + ... Additional Assertions diff --git a/user_guide_src/source/testing/response.rst b/user_guide_src/source/testing/response.rst index 4152feff03d1..e5e0f34852ff 100644 --- a/user_guide_src/source/testing/response.rst +++ b/user_guide_src/source/testing/response.rst @@ -7,8 +7,8 @@ from your test cases. Usually a ``TestResponse`` will be provided for you as a r `Controller Tests `_ or `HTTP Feature Tests `_, but you can always create your own directly using any ``ResponseInterface``:: - $result = new \CodeIgniter\Test\TestResponse($response); - $result->assertOK(); + $result = new \CodeIgniter\Test\TestResponse($response); + $result->assertOK(); Testing the Response ==================== @@ -299,8 +299,8 @@ This method will return the body of the response as a JSON string:: You can use this method to determine if ``$response`` actually holds JSON content:: - // Verify the response is JSON - $this->assertTrue($result->getJSON() !== false) + // Verify the response is JSON + $this->assertTrue($result->getJSON() !== false) .. note:: Be aware that the JSON string will be pretty-printed in the result. From b57266c4be8e0c5a2e9fe5f9046cf5305ee169d6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Sep 2021 12:10:10 +0900 Subject: [PATCH 128/490] docs: fix typo in Factories --- system/Config/Factories.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 8aa779220aed..8bcef3e09610 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -17,7 +17,7 @@ /** * Factories for creating instances. * - * Factories allows dynamic loading of components by their path + * Factories allow dynamic loading of components by their path * and name. The "shared instance" implementation provides a * large performance boost and helps keep code clean of lengthy * instantiation checks. From 0261a26622b6353114af72a8a03de0e5b069f4f1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Sep 2021 14:39:46 +0900 Subject: [PATCH 129/490] docs: fix api responses * docs: add a space before and after `=` * docs: replace tab with space * docs: fix description and RST format * docs: add `$this->` and empty line --- .../source/outgoing/api_responses.rst | 93 +++++++++++-------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/user_guide_src/source/outgoing/api_responses.rst b/user_guide_src/source/outgoing/api_responses.rst index 4758fe4f02bb..99e89783f7d5 100644 --- a/user_guide_src/source/outgoing/api_responses.rst +++ b/user_guide_src/source/outgoing/api_responses.rst @@ -43,29 +43,40 @@ In this example, an HTTP status code of 201 is returned, with the generic status exist for the most common use cases:: // Generic response method - respond($data, 200); + $this->respond($data, 200); + // Generic failure response - fail($errors, 400); + $this->fail($errors, 400); + // Item created response - respondCreated($data); + $this->respondCreated($data); + // Item successfully deleted - respondDeleted($data); + $this->respondDeleted($data); + // Command executed by no response required - respondNoContent($message); + $this->respondNoContent($message); + // Client isn't authorized - failUnauthorized($description); + $this->failUnauthorized($description); + // Forbidden action - failForbidden($description); + $this->failForbidden($description); + // Resource Not Found - failNotFound($description); + $this->failNotFound($description); + // Data did not validate - failValidationError($description); + $this->failValidationError($description); + // Resource already exists - failResourceExists($description); + $this->failResourceExists($description); + // Resource previously deleted - failResourceGone($description); + $this->failResourceGone($description); + // Client made too many requests - failTooManyRequests($description); + $this->failTooManyRequests($description); *********************** Handling Response Types @@ -74,10 +85,10 @@ Handling Response Types When you pass your data in any of these methods, they will determine the data type to format the results as based on the following criteria: -* If $data is a string, it will be treated as HTML to send back to the client. -* If $data is an array, it will be formatted according to the controller's ``$this->format`` value. If that is empty - it will try to negotiate the content type with what the client asked for, defaulting to JSON - if nothing else has been specified within Config\API.php, the ``$supportedResponseFormats`` property. +* If data is a string, it will be treated as HTML to send back to the client. +* If data is an array, it will be formatted according to the controller's ``$this->format`` value. If that is empty, + it will try to negotiate the content type with what the client asked for, defaulting to JSON + if nothing else has been specified within **Config/Format.php**, the ``$supportedResponseFormats`` property. To define the formatter that is used, edit **Config/Format.php**. The ``$supportedResponseFormats`` contains a list of mime types that your application can automatically format the response for. By default, the system knows how to @@ -119,7 +130,7 @@ Class Reference return $this->setResponseFormat('json')->respond(['error' => false]); -.. php:method:: respond($data[, $statusCode=200[, $message='']]) +.. php:method:: respond($data[, $statusCode = 200[, $message = '']]) :param mixed $data: The data to return to the client. Either string or array. :param int $statusCode: The HTTP status code to return. Defaults to 200 @@ -138,7 +149,7 @@ Class Reference .. note:: Since it sets the status code and body on the active Response instance, this should always be the final method in the script execution. -.. php:method:: fail($messages[, int $status=400[, string $code=null[, string $message='']]]) +.. php:method:: fail($messages[, int $status = 400[, string $code = null[, string $message = '']]]) :param mixed $messages: A string or array of strings that contain error messages encountered. :param int $status: The HTTP status code to return. Defaults to 400. @@ -162,14 +173,14 @@ Class Reference The response is an array with two elements: ``error`` and ``messages``. The ``error`` element contains the status code of the error. The ``messages`` element contains an array of error messages. It would look something like:: - $response = [ - 'status' => 400, - 'code' => '321a', - 'messages' => [ - 'Error message 1', - 'Error message 2', - ], - ]; + $response = [ + 'status' => 400, + 'code' => '321a', + 'messages' => [ + 'Error message 1', + 'Error message 2', + ], + ]; .. php:method:: respondCreated($data = null[, string $message = '']) @@ -179,8 +190,8 @@ Class Reference Sets the appropriate status code to use when a new resource was created, typically 201.:: - $user = $userModel->insert($data); - return $this->respondCreated($user); + $user = $userModel->insert($data); + return $this->respondCreated($user); .. php:method:: respondDeleted($data = null[, string $message = '']) @@ -192,8 +203,8 @@ Class Reference :: - $user = $userModel->delete($id); - return $this->respondDeleted(['id' => $id]); + $user = $userModel->delete($id); + return $this->respondDeleted(['id' => $id]); .. php:method:: respondNoContent(string $message = 'No Content') @@ -205,10 +216,10 @@ Class Reference :: - sleep(1); - return $this->respondNoContent(); + sleep(1); + return $this->respondNoContent(); -.. php:method:: failUnauthorized(string $description = 'Unauthorized'[, string $code=null[, string $message = '']]) +.. php:method:: failUnauthorized(string $description = 'Unauthorized'[, string $code = null[, string $message = '']]) :param string $description: The error message to show the user. :param string $code: A custom, API-specific, error code. @@ -220,7 +231,7 @@ Class Reference :: - return $this->failUnauthorized('Invalid Auth token'); + return $this->failUnauthorized('Invalid Auth token'); .. php:method:: failForbidden(string $description = 'Forbidden'[, string $code=null[, string $message = '']]) @@ -235,7 +246,7 @@ Class Reference :: - return $this->failForbidden('Invalid API endpoint.'); + return $this->failForbidden('Invalid API endpoint.'); .. php:method:: failNotFound(string $description = 'Not Found'[, string $code=null[, string $message = '']]) @@ -248,7 +259,7 @@ Class Reference :: - return $this->failNotFound('User 13 cannot be found.'); + return $this->failNotFound('User 13 cannot be found.'); .. php:method:: failValidationErrors($errors[, string $code=null[, string $message = '']]) @@ -261,7 +272,7 @@ Class Reference :: - return $this->failValidationErrors($validation->getErrors()); + return $this->failValidationErrors($validation->getErrors()); .. php:method:: failResourceExists(string $description = 'Conflict'[, string $code=null[, string $message = '']]) @@ -275,7 +286,7 @@ Class Reference :: - return $this->failResourceExists('A user already exists with that email.'); + return $this->failResourceExists('A user already exists with that email.'); .. php:method:: failResourceGone(string $description = 'Gone'[, string $code=null[, string $message = '']]) @@ -289,7 +300,7 @@ Class Reference :: - return $this->failResourceGone('That user has been previously deleted.'); + return $this->failResourceGone('That user has been previously deleted.'); .. php:method:: failTooManyRequests(string $description = 'Too Many Requests'[, string $code=null[, string $message = '']]) @@ -303,7 +314,7 @@ Class Reference :: - return $this->failTooManyRequests('You must wait 15 seconds before making another request.'); + return $this->failTooManyRequests('You must wait 15 seconds before making another request.'); .. php:method:: failServerError(string $description = 'Internal Server Error'[, string $code = null[, string $message = '']]) @@ -316,4 +327,4 @@ Class Reference :: - return $this->failServerError('Server error.'); + return $this->failServerError('Server error.'); From 3ab742eb6ab6d655e357c8a35493d2b84d63c643 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Sep 2021 18:42:02 +0900 Subject: [PATCH 130/490] chore: remove --using-cache=no from php-cs-fixer --- admin/pre-commit | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/admin/pre-commit b/admin/pre-commit index d05724d58146..ffa9936ca9c9 100644 --- a/admin/pre-commit +++ b/admin/pre-commit @@ -52,9 +52,9 @@ if [ "$FILES" != "" ]; then # Run on whole codebase to skip on unnecessary filtering # Run first on app, admin, public if [ -d /proc/cygdrive ]; then - ./vendor/bin/php-cs-fixer fix --verbose --dry-run --using-cache=no --diff --config=.no-header.php-cs-fixer.dist.php + ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.no-header.php-cs-fixer.dist.php else - php ./vendor/bin/php-cs-fixer fix --verbose --dry-run --using-cache=no --diff --config=.no-header.php-cs-fixer.dist.php + php ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.no-header.php-cs-fixer.dist.php fi if [ $? != 0 ]; then @@ -64,9 +64,9 @@ if [ "$FILES" != "" ]; then # Next, run on system, tests, utils, and root PHP files if [ -d /proc/cygdrive ]; then - ./vendor/bin/php-cs-fixer fix --verbose --dry-run --using-cache=no --diff + ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff else - php ./vendor/bin/php-cs-fixer fix --verbose --dry-run --using-cache=no --diff + php ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff fi if [ $? != 0 ]; then From ec6288615fb5f19bd4c00a7926d70ab04cf239d7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 16 Sep 2021 21:50:52 +0900 Subject: [PATCH 131/490] docs: fix cli library --- user_guide_src/source/cli/cli_library.rst | 28 +++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/user_guide_src/source/cli/cli_library.rst b/user_guide_src/source/cli/cli_library.rst index 41638272eaa5..6ba08ab80554 100644 --- a/user_guide_src/source/cli/cli_library.rst +++ b/user_guide_src/source/cli/cli_library.rst @@ -59,7 +59,7 @@ Finally, you can pass :ref:`validation ` rules to the answer input a Validation rules can also be written in the array syntax.:: - $email = CLI::prompt('What is your email?', null, ['required', 'valid_email']); + $email = CLI::prompt('What is your email?', null, ['required', 'valid_email']); **promptByKey()** @@ -199,6 +199,15 @@ on the right with their descriptions. By default, this will wrap back to the lef doesn't allow things to line up in columns. In cases like this, you can pass in a number of spaces to pad every line after the first line, so that you will have a crisp column edge on the left:: + $titles = [ + 'task1a', + 'task1abc', + ]; + $descriptions = [ + 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + "Lorem Ipsum has been the industry's standard dummy text ever since the", + ]; + // Determine the maximum length of all titles // to determine the width of the left column $maxlen = max(array_map('strlen', $titles)); @@ -206,7 +215,11 @@ every line after the first line, so that you will have a crisp column edge on th for ($i = 0; $i < count($titles); $i++) { CLI::write( // Display the title on the left of the row - $titles[$i] . ' ' . + substr( + $titles[$i] . str_repeat(' ', $maxlen + 3), + 0, + $maxlen + 3 + ) . // Wrap the descriptions in a right-hand column // with its left side 3 characters wider than // the longest item on the left. @@ -218,11 +231,12 @@ Would create something like this: .. code-block:: none - task1a Lorem Ipsum is simply dummy - text of the printing and typesetting - industry. - task1abc Lorem Ipsum has been the industry's - standard dummy text ever since the + task1a Lorem Ipsum is simply dummy + text of the printing and + typesetting industry. + task1abc Lorem Ipsum has been the + industry's standard dummy + text ever since the **newLine()** From 640a11e3731205ddf6c882734c08d8db23cb4fa7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 16 Sep 2021 21:52:39 +0900 Subject: [PATCH 132/490] chore: add `composer cs` and `composer cs-fix` --- composer.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 13454e9d77ec..25ec1512ad23 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,21 @@ "bash -c \"if [ -f admin/setup.sh ]; then bash admin/setup.sh; fi\"" ], "analyze": "phpstan analyse", - "test": "phpunit" + "test": "phpunit", + "cs": [ + "php-cs-fixer fix --verbose --dry-run --diff --config=.no-header.php-cs-fixer.dist.php", + "php-cs-fixer fix --verbose --dry-run --diff" + ], + "cs-fix": [ + "php-cs-fixer fix --verbose --diff --config=.no-header.php-cs-fixer.dist.php", + "php-cs-fixer fix --verbose --diff" + ] + }, + "scripts-descriptions": { + "analyze": "Run static analysis", + "test": "Run unit tests", + "cs": "Check the coding style", + "cs-fix": "Fix the coding style" }, "support": { "forum": "http://forum.codeigniter.com/", From 8f81ebad484ba10f64cb5d4194b43a048165ce43 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 17 Sep 2021 05:11:14 +0700 Subject: [PATCH 133/490] [Rector] Apply Rector: SimplifyEmptyArrayCheckRector --- rector.php | 2 ++ system/View/Cell.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/rector.php b/rector.php index 931b89304416..aa9546d37ca9 100644 --- a/rector.php +++ b/rector.php @@ -9,6 +9,7 @@ * the LICENSE file that was distributed with this source code. */ +use Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector; use Rector\CodeQuality\Rector\Expression\InlineIfToExplicitIfRector; use Rector\CodeQuality\Rector\For_\ForToForeachRector; use Rector\CodeQuality\Rector\Foreach_\UnusedForeachValueToArrayKeysRector; @@ -131,4 +132,5 @@ $services->set(FuncGetArgsToVariadicParamRector::class); $services->set(MakeInheritedMethodVisibilitySameAsParentRector::class); $services->set(FixClassCaseSensitivityNameRector::class); + $services->set(SimplifyEmptyArrayCheckRector::class); }; diff --git a/system/View/Cell.php b/system/View/Cell.php index 7affd2742049..50168a153be6 100644 --- a/system/View/Cell.php +++ b/system/View/Cell.php @@ -174,7 +174,7 @@ public function prepareParams($params) unset($newParams); } - if (is_array($params) && empty($params)) { + if ($params === []) { return []; } From e9c452e3c92e77f2ec07c43bee7fc7f70d4bc9f2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 17 Sep 2021 18:04:30 +0900 Subject: [PATCH 134/490] docs: replace tab with space --- .../source/extending/basecontroller.rst | 38 ++-- .../source/extending/core_classes.rst | 16 +- .../source/helpers/cookie_helper.rst | 28 +-- user_guide_src/source/helpers/xml_helper.rst | 2 +- .../source/incoming/controllers.rst | 2 +- .../source/libraries/curlrequest.rst | 196 +++++++++--------- .../source/outgoing/alternative_php.rst | 28 +-- 7 files changed, 155 insertions(+), 155 deletions(-) diff --git a/user_guide_src/source/extending/basecontroller.rst b/user_guide_src/source/extending/basecontroller.rst index 424329257fc1..bd5a76bf2386 100644 --- a/user_guide_src/source/extending/basecontroller.rst +++ b/user_guide_src/source/extending/basecontroller.rst @@ -6,16 +6,16 @@ CodeIgniter's core Controller should not be changed, but a default class extensi **app/Controllers/BaseController.php**. Any new controllers you make should extend ``BaseController`` to take advantage of preloaded components and any additional functionality you provide:: - session = \Config\Services::session(); - } + public function initController(...) + { + // Do Not Edit This Line + parent::initController($request, $response, $logger); + + $this->session = \Config\Services::session(); + } Additional Methods ================== @@ -54,7 +54,7 @@ the public controllers and make ``AdminController`` for any administrative contr If you do not want to use the base controller you may bypass it by having your controllers extend the system Controller instead:: - class Home extends \CodeIgniter\Controller - { - - } + class Home extends \CodeIgniter\Controller + { + + } diff --git a/user_guide_src/source/extending/core_classes.rst b/user_guide_src/source/extending/core_classes.rst index 6d7b0c9a3b64..2e856f1eeaed 100644 --- a/user_guide_src/source/extending/core_classes.rst +++ b/user_guide_src/source/extending/core_classes.rst @@ -62,14 +62,14 @@ the core system class, you would create your class like this:: Then you would modify the ``routes`` service to load your class instead:: - public static function routes(bool $getShared = true) - { - if ($getShared) { - return static::getSharedInstance('routes'); - } - - return new RouteCollection(static::locator(), config('Modules')); - } + public static function routes(bool $getShared = true) + { + if ($getShared) { + return static::getSharedInstance('routes'); + } + + return new RouteCollection(static::locator(), config('Modules')); + } Extending Core Classes ====================== diff --git a/user_guide_src/source/helpers/cookie_helper.rst b/user_guide_src/source/helpers/cookie_helper.rst index 4175bb2b99c4..2a7f0e6e6c43 100755 --- a/user_guide_src/source/helpers/cookie_helper.rst +++ b/user_guide_src/source/helpers/cookie_helper.rst @@ -22,16 +22,16 @@ The following functions are available: .. php:function:: set_cookie($name[, $value = ''[, $expire = ''[, $domain = ''[, $path = '/'[, $prefix = ''[, $secure = false[, $httpOnly = false[, $sameSite = '']]]]]]]]) - :param mixed $name: Cookie name *or* associative array of all of the parameters available to this function - :param string $value: Cookie value - :param int $expire: Number of seconds until expiration - :param string $domain: Cookie domain (usually: .yourdomain.com) - :param string $path: Cookie path - :param string $prefix: Cookie name prefix - :param bool $secure: Whether to only send the cookie through HTTPS - :param bool $httpOnly: Whether to hide the cookie from JavaScript - :param string $sameSite: The value for the SameSite cookie parameter. If null, the default from `config/App.php` is used - :rtype: void + :param mixed $name: Cookie name *or* associative array of all of the parameters available to this function + :param string $value: Cookie value + :param int $expire: Number of seconds until expiration + :param string $domain: Cookie domain (usually: .yourdomain.com) + :param string $path: Cookie path + :param string $prefix: Cookie name prefix + :param bool $secure: Whether to only send the cookie through HTTPS + :param bool $httpOnly: Whether to hide the cookie from JavaScript + :param string $sameSite: The value for the SameSite cookie parameter. If null, the default from `config/App.php` is used + :rtype: void This helper function gives you friendlier syntax to set browser cookies. Refer to the :doc:`Response Library ` for @@ -40,10 +40,10 @@ The following functions are available: .. php:function:: get_cookie($index[, $xssClean = false]) - :param string $index: Cookie name - :param bool $xss_clean: Whether to apply XSS filtering to the returned value - :returns: The cookie value or null if not found - :rtype: mixed + :param string $index: Cookie name + :param bool $xss_clean: Whether to apply XSS filtering to the returned value + :returns: The cookie value or null if not found + :rtype: mixed This helper function gives you friendlier syntax to get browser cookies. Refer to the :doc:`IncomingRequest Library ` for diff --git a/user_guide_src/source/helpers/xml_helper.rst b/user_guide_src/source/helpers/xml_helper.rst index 4f7ff453384a..ebccaca79f91 100644 --- a/user_guide_src/source/helpers/xml_helper.rst +++ b/user_guide_src/source/helpers/xml_helper.rst @@ -27,7 +27,7 @@ The following functions are available: :param string $str: the text string to convert :param bool $protect_all: Whether to protect all content that looks like a potential entity instead of just numbered entities, e.g., &foo; :returns: XML-converted string - :rtype: string + :rtype: string Takes a string as input and converts the following reserved XML characters to entities: diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index 49075fc84500..dfca82fea06e 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -262,7 +262,7 @@ if you were to define a method like this for the `Helloworld` controller:: then trying to access it using the following URL will not work:: - example.com/index.php/helloworld/utility/ + example.com/index.php/helloworld/utility/ Organizing Your Controllers into Sub-directories ================================================ diff --git a/user_guide_src/source/libraries/curlrequest.rst b/user_guide_src/source/libraries/curlrequest.rst index 08dc0c0a41c9..c9c69ea7ae08 100644 --- a/user_guide_src/source/libraries/curlrequest.rst +++ b/user_guide_src/source/libraries/curlrequest.rst @@ -27,27 +27,27 @@ The library can be loaded either manually or through the :doc:`Services class 'http://example.com/api/v1/', - 'timeout' => 3, - ]; - $client = \Config\Services::curlrequest($options); + $options = [ + 'baseURI' => 'http://example.com/api/v1/', + 'timeout' => 3, + ]; + $client = \Config\Services::curlrequest($options); When creating the class manually, you need to pass a few dependencies in. The first parameter is an instance of the ``Config\App`` class. The second parameter is a URI instance. The third parameter is a Response object. The fourth parameter is the optional ``$options`` array:: - $client = new \CodeIgniter\HTTP\CURLRequest( - new \Config\App(), - new \CodeIgniter\HTTP\URI(), - new \CodeIgniter\HTTP\Response(new \Config\App()), - $options - ); + $client = new \CodeIgniter\HTTP\CURLRequest( + new \Config\App(), + new \CodeIgniter\HTTP\URI(), + new \CodeIgniter\HTTP\Response(new \Config\App()), + $options + ); ************************ Working with the Library @@ -64,19 +64,19 @@ Most communication is done through the ``request()`` method, which fires off the a Response instance to you. This takes the HTTP method, the url and an array of options as the parameters. :: - $client = \Config\Services::curlrequest(); + $client = \Config\Services::curlrequest(); - $response = $client->request('GET', 'https://api.github.com/user', [ - 'auth' => ['user', 'pass'], - ]); + $response = $client->request('GET', 'https://api.github.com/user', [ + 'auth' => ['user', 'pass'], + ]); Since the response is an instance of ``CodeIgniter\HTTP\Response`` you have all of the normal information available to you:: - echo $response->getStatusCode(); - echo $response->getBody(); - echo $response->getHeader('Content-Type'); - $language = $response->negotiateLanguage(['en', 'fr']); + echo $response->getStatusCode(); + echo $response->getBody(); + echo $response->getHeader('Content-Type'); + $language = $response->negotiateLanguage(['en', 'fr']); While the ``request()`` method is the most flexible, you can also use the following shortcut methods. They each take the URL as the first parameter and an array of options as the second:: @@ -96,31 +96,31 @@ A ``baseURI`` can be set as one of the options during the instantiation of the c set a base URI, and then make all requests with that client using relative URLs. This is especially handy when working with APIs:: - $client = \Config\Services::curlrequest([ - 'baseURI' => 'https://example.com/api/v1/', - ]); + $client = \Config\Services::curlrequest([ + 'baseURI' => 'https://example.com/api/v1/', + ]); - // GET http:example.com/api/v1/photos - $client->get('photos'); + // GET http:example.com/api/v1/photos + $client->get('photos'); - // GET http:example.com/api/v1/photos/13 - $client->delete('photos/13'); + // GET http:example.com/api/v1/photos/13 + $client->delete('photos/13'); When a relative URI is provided to the ``request()`` method or any of the shortcut methods, it will be combined with the baseURI according to the rules described by `RFC 2986, section 2 `_. To save you some time, here are some examples of how the combinations are resolved. - ===================== ================ ======================== - baseURI URI Result - ===================== ================ ======================== - `http://foo.com` /bar `http://foo.com/bar` - `http://foo.com/foo` /bar `http://foo.com/bar` - `http://foo.com/foo` bar `http://foo.com/bar` - `http://foo.com/foo/` bar `http://foo.com/foo/bar` - `http://foo.com` `http://baz.com` `http://baz.com` - `http://foo.com/?bar` bar `http://foo.com/bar` - ===================== ================ ======================== + ===================== ================ ======================== + baseURI URI Result + ===================== ================ ======================== + `http://foo.com` /bar `http://foo.com/bar` + `http://foo.com/foo` /bar `http://foo.com/bar` + `http://foo.com/foo` bar `http://foo.com/bar` + `http://foo.com/foo/` bar `http://foo.com/foo/bar` + `http://foo.com` `http://baz.com` `http://baz.com` + `http://foo.com/?bar` bar `http://foo.com/bar` + ===================== ================ ======================== Using Responses =============== @@ -130,29 +130,29 @@ methods. The most commonly used methods let you determine the response itself. You can get the status code and reason phrase of the response:: - $code = $response->getStatusCode(); // 200 - $reason = $response->getReason(); // OK + $code = $response->getStatusCode(); // 200 + $reason = $response->getReason(); // OK You can retrieve headers from the response:: - // Get a header line - echo $response->getHeaderLine('Content-Type'); + // Get a header line + echo $response->getHeaderLine('Content-Type'); - // Get all headers - foreach ($response->getHeaders() as $name => $value) { - echo $name .': '. $response->getHeaderLine($name) ."\n"; - } + // Get all headers + foreach ($response->getHeaders() as $name => $value) { + echo $name .': '. $response->getHeaderLine($name) ."\n"; + } The body can be retrieved using the ``getBody()`` method:: - $body = $response->getBody(); + $body = $response->getBody(); The body is the raw body provided by the remote getServer. If the content type requires formatting, you will need to ensure that your script handles that:: - if (strpos($response->getHeader('content-type'), 'application/json') !== false) { - $body = json_decode($body); - } + if (strpos($response->getHeader('content-type'), 'application/json') !== false) { + $body = json_decode($body); + } *************** Request Options @@ -169,23 +169,23 @@ allows you to modify how that works. If you set the value to ``false``, then it will not follow any redirects at all:: - $client->request('GET', 'http://example.com', ['allow_redirects' => false]); + $client->request('GET', 'http://example.com', ['allow_redirects' => false]); Setting it to ``true`` will apply the default settings to the request:: - $client->request('GET', 'http://example.com', ['allow_redirects' => true]); + $client->request('GET', 'http://example.com', ['allow_redirects' => true]); - // Sets the following defaults: - 'max' => 5, // Maximum number of redirects to follow before stopping - 'strict' => true, // Ensure POST requests stay POST requests through redirects - 'protocols' => ['http', 'https'] // Restrict redirects to one or more protocols + // Sets the following defaults: + 'max' => 5, // Maximum number of redirects to follow before stopping + 'strict' => true, // Ensure POST requests stay POST requests through redirects + 'protocols' => ['http', 'https'] // Restrict redirects to one or more protocols You can pass in array as the value of the ``allow_redirects`` option to specify new settings in place of the defaults:: - $client->request('GET', 'http://example.com', ['allow_redirects' => [ - 'max' => 10, - 'protocols' => ['https'] // Force HTTPS domains only. - ]]); + $client->request('GET', 'http://example.com', ['allow_redirects' => [ + 'max' => 10, + 'protocols' => ['https'] // Force HTTPS domains only. + ]]); .. note:: Following redirects does not work when PHP is in safe_mode or open_basedir is enabled. @@ -198,7 +198,7 @@ Digest authentication - this simply passes the username and password along for y array where the first element is the username, and the second is the password. The third parameter should be the type of authentication to use, either ``basic`` or ``digest``:: - $client->request('GET', 'http://example.com', ['auth' => ['username', 'password', 'digest']]); + $client->request('GET', 'http://example.com', ['auth' => ['username', 'password', 'digest']]); body ==== @@ -206,12 +206,12 @@ body There are two ways to set the body of the request for request types that support them, like PUT, OR POST. The first way is to use the ``setBody()`` method:: - $client->setBody($body) ->request('put', 'http://example.com'); + $client->setBody($body) ->request('put', 'http://example.com'); The second method is by passing a ``body`` option in. This is provided to maintain Guzzle API compatibility, and functions the exact same way as the previous example. The value must be a string:: - $client->request('put', 'http://example.com', ['body' => $body]); + $client->request('put', 'http://example.com', ['body' => $body]); cert ==== @@ -229,7 +229,7 @@ By default, CodeIgniter does not impose a limit for cURL to attempt to connect t modify this value, you can do so by passing the amount of time in seconds with the ``connect_timeout`` option. You can pass 0 to wait indefinitely:: - $response->request('GET', 'http://example.com', ['connect_timeout' => 0]); + $response->request('GET', 'http://example.com', ['connect_timeout' => 0]); cookie ====== @@ -238,7 +238,7 @@ This specifies the filename that CURL should use to read cookie values from, and to save cookie values to. This is done using the CURL_COOKIEJAR and CURL_COOKIEFILE options. An example:: - $response->request('GET', 'http://example.com', ['cookie' => WRITEPATH . 'CookieSaver.txt']); + $response->request('GET', 'http://example.com', ['cookie' => WRITEPATH . 'CookieSaver.txt']); debug ===== @@ -248,19 +248,19 @@ script execution. This is done by passing CURLOPT_VERBOSE and echoing the output server via ``spark serve`` you will see the output in the console. Otherwise, the output will be written to the server's error log. - $response->request('GET', 'http://example.com', ['debug' => true]); + $response->request('GET', 'http://example.com', ['debug' => true]); You can pass a filename as the value for debug to have the output written to a file:: - $response->request('GET', 'http://example.com', ['debug' => '/usr/local/curl_log.txt']); + $response->request('GET', 'http://example.com', ['debug' => '/usr/local/curl_log.txt']); delay ===== Allows you to pause a number of milliseconds before sending the request:: - // Delay for 2 seconds - $response->request('GET', 'http://example.com', ['delay' => 2000]); + // Delay for 2 seconds + $response->request('GET', 'http://example.com', ['delay' => 2000]); form_params =========== @@ -269,12 +269,12 @@ You can send form data in an application/x-www-form-urlencoded POST request by p the ``form_params`` option. This will set the ``Content-Type`` header to ``application/x-www-form-urlencoded`` if it's not already set:: - $client->request('POST', '/post', [ - 'form_params' => [ - 'foo' => 'bar', - 'baz' => ['hi', 'there'], - ], - ]); + $client->request('POST', '/post', [ + 'form_params' => [ + 'foo' => 'bar', + 'baz' => ['hi', 'there'], + ], + ]); .. note:: ``form_params`` cannot be used with the ``multipart`` option. You will need to use one or the other. Use ``form_params`` for ``application/x-www-form-urlencoded`` request, and ``multipart`` for ``multipart/form-data`` @@ -287,13 +287,13 @@ While you can set any headers this request needs by using the ``setHeader()`` me array of headers in as an option. Each key is the name of a header, and each value is a string or array of strings representing the header field values:: - $client->request('get', '/', [ - 'headers' => [ - 'User-Agent' => 'testing/1.0', - 'Accept' => 'application/json', - 'X-Foo' => ['Bar', 'Baz'], - ], - ]); + $client->request('get', '/', [ + 'headers' => [ + 'User-Agent' => 'testing/1.0', + 'Accept' => 'application/json', + 'X-Foo' => ['Bar', 'Baz'], + ], + ]); If headers are passed into the constructor they are treated as default values that will be overridden later by any further headers arrays or calls to ``setHeader()``. @@ -318,7 +318,7 @@ The ``json`` option is used to easily upload JSON encoded data as the body of a of ``application/json`` is added, overwriting any Content-Type that might be already set. The data provided to this option can be any value that ``json_encode()`` accepts:: - $response = $client->request('PUT', '/put', ['json' => ['foo' => 'bar']]); + $response = $client->request('PUT', '/put', ['json' => ['foo' => 'bar']]); .. note:: This option does not allow for any customization of the ``json_encode()`` function, or the Content-Type header. If you need that ability, you will need to encode the data manually, passing it through the ``setBody()`` @@ -332,10 +332,10 @@ the `CURLFile Class `_. The va of POST data to send. For safer usage, the legacy method of uploading files by prefixing their name with an `@` has been disabled. Any files that you want to send must be passed as instances of CURLFile:: - $post_data = [ - 'foo' => 'bar', - 'userfile' => new \CURLFile('/path/to/file.txt'), - ]; + $post_data = [ + 'foo' => 'bar', + 'userfile' => new \CURLFile('/path/to/file.txt'), + ]; .. note:: ``multipart`` cannot be used with the ``form_params`` option. You can only use one or the other. Use ``form_params`` for ``application/x-www-form-urlencoded`` requests, and ``multipart`` for ``multipart/form-data`` @@ -346,8 +346,8 @@ query You can pass along data to send as query string variables by passing an associative array as the ``query`` option:: - // Send a GET request to /get?foo=bar - $client->request('GET', '/get', ['query' => ['foo' => 'bar']]); + // Send a GET request to /get?foo=bar + $client->request('GET', '/get', ['query' => ['foo' => 'bar']]); timeout ======= @@ -355,14 +355,14 @@ timeout By default, cURL functions are allowed to run as long as they take, with no time limit. You can modify this with the ``timeout`` option. The value should be the number of seconds you want the functions to execute for. Use 0 to wait indefinitely:: - $response->request('GET', 'http://example.com', ['timeout' => 5]); + $response->request('GET', 'http://example.com', ['timeout' => 5]); user_agent ========== Allows specifying the User Agent for requests:: - $response->request('GET', 'http://example.com', ['user_agent' => 'CodeIgniter Framework v4']); + $response->request('GET', 'http://example.com', ['user_agent' => 'CodeIgniter Framework v4']); verify ====== @@ -373,14 +373,14 @@ will disable the certificate verification (this is insecure, and allows man-in-t to a string that contains the path to a CA bundle to enable verification with a custom certificate. The default value is true:: - // Use the system's CA bundle (this is the default setting) - $client->request('GET', '/', ['verify' => true]); + // Use the system's CA bundle (this is the default setting) + $client->request('GET', '/', ['verify' => true]); - // Use a custom SSL certificate on disk. - $client->request('GET', '/', ['verify' => '/path/to/cert.pem']); + // Use a custom SSL certificate on disk. + $client->request('GET', '/', ['verify' => '/path/to/cert.pem']); - // Disable validation entirely. (Insecure!) - $client->request('GET', '/', ['verify' => false]); + // Disable validation entirely. (Insecure!) + $client->request('GET', '/', ['verify' => false]); version ======= @@ -388,5 +388,5 @@ version To set the HTTP protocol to use, you can pass a string or float with the version number (typically either 1.0 or 1.1, 2.0 is currently unsupported.):: - // Force HTTP/1.0 - $client->request('GET', '/', ['version' => 1.0]); + // Force HTTP/1.0 + $client->request('GET', '/', ['version' => 1.0]); diff --git a/user_guide_src/source/outgoing/alternative_php.rst b/user_guide_src/source/outgoing/alternative_php.rst index 702953fee287..70a86e2ebb75 100644 --- a/user_guide_src/source/outgoing/alternative_php.rst +++ b/user_guide_src/source/outgoing/alternative_php.rst @@ -15,11 +15,11 @@ Alternative Echos Normally to echo, or print out a variable you would do this:: - + With the alternative syntax you can instead do it this way:: - + Alternative Control Structures ============================== @@ -27,15 +27,15 @@ Alternative Control Structures Controls structures, like if, for, foreach, and while can be written in a simplified format as well. Here is an example using ``foreach``:: -
      +
        - + -
      • +
      • - + -
      +
    Notice that there are no braces. Instead, the end brace is replaced with ``endforeach``. Each of the control structures listed above has a similar @@ -46,16 +46,16 @@ Also notice that instead of using a semicolon after each structure Here is another example, using ``if``/``elseif``/``else``. Notice the colons:: - + -

    Hi Sally

    +

    Hi Sally

    - + -

    Hi Joe

    +

    Hi Joe

    - + -

    Hi unknown user

    +

    Hi unknown user

    - + From dc5a313f3f3c6e281f003b27d99123e5ad6ac95c Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 21 Sep 2021 14:19:13 +0900 Subject: [PATCH 135/490] docs: replace tab with space --- user_guide_src/source/helpers/text_helper.rst | 158 +++---- user_guide_src/source/libraries/email.rst | 396 +++++++++--------- .../source/libraries/encryption.rst | 172 ++++---- user_guide_src/source/libraries/images.rst | 166 ++++---- user_guide_src/source/libraries/publisher.rst | 388 ++++++++--------- user_guide_src/source/libraries/sessions.rst | 268 ++++++------ .../source/libraries/user_agent.rst | 178 ++++---- user_guide_src/source/outgoing/table.rst | 340 +++++++-------- user_guide_src/source/outgoing/views.rst | 164 ++++---- 9 files changed, 1115 insertions(+), 1115 deletions(-) diff --git a/user_guide_src/source/helpers/text_helper.rst b/user_guide_src/source/helpers/text_helper.rst index 6b875e3d7e36..98a6f3b9e332 100755 --- a/user_guide_src/source/helpers/text_helper.rst +++ b/user_guide_src/source/helpers/text_helper.rst @@ -21,10 +21,10 @@ The following functions are available: .. php:function:: random_string([$type = 'alnum'[, $len = 8]]) - :param string $type: Randomization type - :param int $len: Output string length - :returns: A random string - :rtype: string + :param string $type: Randomization type + :param int $len: Output string length + :returns: A random string + :rtype: string Generates a random string based on the type and length you specify. Useful for creating passwords or generating random hashes. @@ -47,11 +47,11 @@ The following functions are available: .. php:function:: increment_string($str[, $separator = '_'[, $first = 1]]) - :param string $str: Input string - :param string $separator: Separator to append a duplicate number with - :param int $first: Starting number - :returns: An incremented string - :rtype: string + :param string $str: Input string + :param string $separator: Separator to append a duplicate number with + :param int $first: Starting number + :returns: An incremented string + :rtype: string Increments a string by appending a number to it or increasing the number. Useful for creating "copies" or a file or duplicating database @@ -65,9 +65,9 @@ The following functions are available: .. php:function:: alternator($args) - :param mixed $args: A variable number of arguments - :returns: Alternated string(s) - :rtype: mixed + :param mixed $args: A variable number of arguments + :returns: Alternated string(s) + :rtype: mixed Allows two or more items to be alternated between, when cycling through a loop. Example:: @@ -90,9 +90,9 @@ The following functions are available: .. php:function:: reduce_double_slashes($str) - :param string $str: Input string - :returns: A string with normalized slashes - :rtype: string + :param string $str: Input string + :returns: A string with normalized slashes + :rtype: string Converts double slashes in a string to a single slash, except those found in URL protocol prefixes (e.g., http://). @@ -104,9 +104,9 @@ The following functions are available: .. php:function:: strip_slashes($data) - :param mixed $data: Input string or an array of strings - :returns: String(s) with stripped slashes - :rtype: mixed + :param mixed $data: Input string or an array of strings + :returns: String(s) with stripped slashes + :rtype: mixed Removes any slashes from an array of strings. @@ -132,11 +132,11 @@ The following functions are available: .. php:function:: reduce_multiples($str[, $character = ''[, $trim = false]]) - :param string $str: Text to search in - :param string $character: Character to reduce - :param bool $trim: Whether to also trim the specified character - :returns: Reduced string - :rtype: string + :param string $str: Text to search in + :param string $character: Character to reduce + :param bool $trim: Whether to also trim the specified character + :returns: Reduced string + :rtype: string Reduces multiple instances of a particular character occurring directly after each other. Example:: @@ -152,9 +152,9 @@ The following functions are available: .. php:function:: quotes_to_entities($str) - :param string $str: Input string - :returns: String with quotes converted to HTML entities - :rtype: string + :param string $str: Input string + :returns: String with quotes converted to HTML entities + :rtype: string Converts single and double quotes in a string to the corresponding HTML entities. Example:: @@ -164,9 +164,9 @@ The following functions are available: .. php:function:: strip_quotes($str) - :param string $str: Input string - :returns: String with quotes stripped - :rtype: string + :param string $str: Input string + :returns: String with quotes stripped + :rtype: string Removes single and double quotes from a string. Example:: @@ -175,11 +175,11 @@ The following functions are available: .. php:function:: word_limiter($str[, $limit = 100[, $end_char = '…']]) - :param string $str: Input string - :param int $limit: Limit - :param string $end_char: End character (usually an ellipsis) - :returns: Word-limited string - :rtype: string + :param string $str: Input string + :param int $limit: Limit + :param string $end_char: End character (usually an ellipsis) + :returns: Word-limited string + :rtype: string Truncates a string to the number of *words* specified. Example:: @@ -192,11 +192,11 @@ The following functions are available: .. php:function:: character_limiter($str[, $n = 500[, $end_char = '…']]) - :param string $str: Input string - :param int $n: Number of characters - :param string $end_char: End character (usually an ellipsis) - :returns: Character-limited string - :rtype: string + :param string $str: Input string + :param int $n: Number of characters + :param string $end_char: End character (usually an ellipsis) + :returns: Character-limited string + :rtype: string Truncates a string to the number of *characters* specified. It maintains the integrity of words so the character count may be slightly @@ -216,9 +216,9 @@ The following functions are available: .. php:function:: ascii_to_entities($str) - :param string $str: Input string - :returns: A string with ASCII values converted to entities - :rtype: string + :param string $str: Input string + :returns: A string with ASCII values converted to entities + :rtype: string Converts ASCII values to character entities, including high ASCII and MS Word characters that can cause problems when used in a web page, so that @@ -234,19 +234,19 @@ The following functions are available: .. php:function:: entities_to_ascii($str[, $all = true]) - :param string $str: Input string - :param bool $all: Whether to convert unsafe entities as well - :returns: A string with HTML entities converted to ASCII characters - :rtype: string + :param string $str: Input string + :param bool $all: Whether to convert unsafe entities as well + :returns: A string with HTML entities converted to ASCII characters + :rtype: string This function does the opposite of :php:func:`ascii_to_entities()`. It turns character entities back into ASCII. .. php:function:: convert_accented_characters($str) - :param string $str: Input string - :returns: A string with accented characters converted - :rtype: string + :param string $str: Input string + :returns: A string with accented characters converted + :rtype: string Transliterates high ASCII characters to low ASCII equivalents. Useful when non-English characters need to be used where only standard ASCII @@ -262,11 +262,11 @@ The following functions are available: .. php:function:: word_censor($str, $censored[, $replacement = '']) - :param string $str: Input string - :param array $censored: List of bad words to censor - :param string $replacement: What to replace bad words with - :returns: Censored string - :rtype: string + :param string $str: Input string + :param array $censored: List of bad words to censor + :param string $replacement: What to replace bad words with + :returns: Censored string + :rtype: string Enables you to censor words within a text string. The first parameter will contain the original string. The second will contain an array of @@ -281,9 +281,9 @@ The following functions are available: .. php:function:: highlight_code($str) - :param string $str: Input string - :returns: String with code highlighted via HTML - :rtype: string + :param string $str: Input string + :returns: String with code highlighted via HTML + :rtype: string Colorizes a string of code (PHP, HTML, etc.). Example:: @@ -294,12 +294,12 @@ The following functions are available: .. php:function:: highlight_phrase($str, $phrase[, $tag_open = ''[, $tag_close = '']]) - :param string $str: Input string - :param string $phrase: Phrase to highlight - :param string $tag_open: Opening tag used for the highlight - :param string $tag_close: Closing tag for the highlight - :returns: String with a phrase highlighted via HTML - :rtype: string + :param string $str: Input string + :param string $phrase: Phrase to highlight + :param string $tag_open: Opening tag used for the highlight + :param string $tag_close: Closing tag for the highlight + :returns: String with a phrase highlighted via HTML + :rtype: string Will highlight a phrase within a text string. The first parameter will contain the original string, the second will contain the phrase you wish @@ -327,10 +327,10 @@ The following functions are available: .. php:function:: word_wrap($str[, $charlim = 76]) - :param string $str: Input string - :param int $charlim: Character limit - :returns: Word-wrapped string - :rtype: string + :param string $str: Input string + :param int $charlim: Character limit + :returns: Word-wrapped string + :rtype: string Wraps text at the specified *character* count while maintaining complete words. @@ -350,12 +350,12 @@ The following functions are available: .. php:function:: ellipsize($str, $max_length[, $position = 1[, $ellipsis = '…']]) - :param string $str: Input string - :param int $max_length: String length limit - :param mixed $position: Position to split at (int or float) - :param string $ellipsis: What to use as the ellipsis character - :returns: Ellipsized string - :rtype: string + :param string $str: Input string + :param int $max_length: String length limit + :param mixed $position: Position to split at (int or float) + :param string $ellipsis: What to use as the ellipsis character + :returns: Ellipsized string + :rtype: string This function will strip tags from a string, split it at a defined maximum length, and insert an ellipsis. @@ -380,12 +380,12 @@ The following functions are available: .. php:function:: excerpt($text, $phrase = false, $radius = 100, $ellipsis = '...') - :param string $text: Text to extract an excerpt - :param string $phrase: Phrase or word to extract the text around - :param int $radius: Number of characters before and after $phrase - :param string $ellipsis: What to use as the ellipsis character - :returns: Excerpt. - :rtype: string + :param string $text: Text to extract an excerpt + :param string $phrase: Phrase or word to extract the text around + :param int $radius: Number of characters before and after $phrase + :param string $ellipsis: What to use as the ellipsis character + :returns: Excerpt. + :rtype: string This function will extract $radius number of characters before and after the central $phrase with an ellipsis before and after. diff --git a/user_guide_src/source/libraries/email.rst b/user_guide_src/source/libraries/email.rst index 55604ad6bbee..f57f3cf93d8d 100644 --- a/user_guide_src/source/libraries/email.rst +++ b/user_guide_src/source/libraries/email.rst @@ -32,17 +32,17 @@ set your preferences in the **app/Config/Email.php** file. Here is a basic example demonstrating how you might send email:: - $email = \Config\Services::email(); + $email = \Config\Services::email(); - $email->setFrom('your@example.com', 'Your Name'); - $email->setTo('someone@example.com'); - $email->setCC('another@another-example.com'); - $email->setBCC('them@their-example.com'); + $email->setFrom('your@example.com', 'Your Name'); + $email->setTo('someone@example.com'); + $email->setCC('another@another-example.com'); + $email->setBCC('them@their-example.com'); - $email->setSubject('Email Test'); - $email->setMessage('Testing the email class.'); + $email->setSubject('Email Test'); + $email->setMessage('Testing the email class.'); - $email->send(); + $email->send(); Setting Email Preferences ========================= @@ -56,15 +56,15 @@ Preferences are set by passing an array of preference values to the email initialize method. Here is an example of how you might set some preferences:: - $config['protocol'] = 'sendmail'; - $config['mailPath'] = '/usr/sbin/sendmail'; - $config['charset'] = 'iso-8859-1'; - $config['wordWrap'] = true; + $config['protocol'] = 'sendmail'; + $config['mailPath'] = '/usr/sbin/sendmail'; + $config['charset'] = 'iso-8859-1'; + $config['wordWrap'] = true; - $email->initialize($config); + $email->initialize($config); .. note:: Most of the preferences have default values that will be used - if you do not set them. + if you do not set them. Setting Email Preferences in a Config File ------------------------------------------ @@ -157,13 +157,13 @@ causing it to become un-clickable by the person receiving it. CodeIgniter lets you manually override word wrapping within part of your message like this:: - The text of your email that - gets wrapped normally. + The text of your email that + gets wrapped normally. - {unwrap}http://example.com/a_long_link_that_should_not_be_wrapped.html{/unwrap} + {unwrap}http://example.com/a_long_link_that_should_not_be_wrapped.html{/unwrap} - More text that will be - wrapped normally. + More text that will be + wrapped normally. Place the item you do not want word-wrapped between: {unwrap} {/unwrap} @@ -174,266 +174,266 @@ Class Reference .. php:class:: CodeIgniter\\Email\\Email - .. php:method:: setFrom($from[, $name = ''[, $returnPath = null]]) + .. php:method:: setFrom($from[, $name = ''[, $returnPath = null]]) - :param string $from: "From" e-mail address - :param string $name: "From" display name - :param string $returnPath: Optional email address to redirect undelivered e-mail to - :returns: CodeIgniter\\Email\\Email instance (method chaining) - :rtype: CodeIgniter\\Email\\Email + :param string $from: "From" e-mail address + :param string $name: "From" display name + :param string $returnPath: Optional email address to redirect undelivered e-mail to + :returns: CodeIgniter\\Email\\Email instance (method chaining) + :rtype: CodeIgniter\\Email\\Email - Sets the email address and name of the person sending the email:: + Sets the email address and name of the person sending the email:: - $email->setFrom('you@example.com', 'Your Name'); + $email->setFrom('you@example.com', 'Your Name'); - You can also set a Return-Path, to help redirect undelivered mail:: + You can also set a Return-Path, to help redirect undelivered mail:: - $email->setFrom('you@example.com', 'Your Name', 'returned_emails@example.com'); + $email->setFrom('you@example.com', 'Your Name', 'returned_emails@example.com'); - .. note:: Return-Path can't be used if you've configured 'smtp' as - your protocol. + .. note:: Return-Path can't be used if you've configured 'smtp' as + your protocol. - .. php:method:: setReplyTo($replyto[, $name = '']) + .. php:method:: setReplyTo($replyto[, $name = '']) - :param string $replyto: E-mail address for replies - :param string $name: Display name for the reply-to e-mail address - :returns: CodeIgniter\\Email\\Email instance (method chaining) - :rtype: CodeIgniter\\Email\\Email + :param string $replyto: E-mail address for replies + :param string $name: Display name for the reply-to e-mail address + :returns: CodeIgniter\\Email\\Email instance (method chaining) + :rtype: CodeIgniter\\Email\\Email - Sets the reply-to address. If the information is not provided the - information in the `setFrom <#setFrom>`_ method is used. Example:: + Sets the reply-to address. If the information is not provided the + information in the `setFrom <#setFrom>`_ method is used. Example:: - $email->setReplyTo('you@example.com', 'Your Name'); + $email->setReplyTo('you@example.com', 'Your Name'); - .. php:method:: setTo($to) + .. php:method:: setTo($to) - :param mixed $to: Comma-delimited string or an array of e-mail addresses - :returns: CodeIgniter\\Email\\Email instance (method chaining) - :rtype: CodeIgniter\\Email\\Email + :param mixed $to: Comma-delimited string or an array of e-mail addresses + :returns: CodeIgniter\\Email\\Email instance (method chaining) + :rtype: CodeIgniter\\Email\\Email - Sets the email address(s) of the recipient(s). Can be a single e-mail, - a comma-delimited list or an array:: + Sets the email address(s) of the recipient(s). Can be a single e-mail, + a comma-delimited list or an array:: - $email->setTo('someone@example.com'); + $email->setTo('someone@example.com'); - :: + :: - $email->setTo('one@example.com, two@example.com, three@example.com'); + $email->setTo('one@example.com, two@example.com, three@example.com'); - :: + :: - $email->setTo(['one@example.com', 'two@example.com', 'three@example.com']); + $email->setTo(['one@example.com', 'two@example.com', 'three@example.com']); - .. php:method:: setCC($cc) + .. php:method:: setCC($cc) - :param mixed $cc: Comma-delimited string or an array of e-mail addresses - :returns: CodeIgniter\\Email\\Email instance (method chaining) - :rtype: CodeIgniter\\Email\\Email + :param mixed $cc: Comma-delimited string or an array of e-mail addresses + :returns: CodeIgniter\\Email\\Email instance (method chaining) + :rtype: CodeIgniter\\Email\\Email - Sets the CC email address(s). Just like the "to", can be a single e-mail, - a comma-delimited list or an array. + Sets the CC email address(s). Just like the "to", can be a single e-mail, + a comma-delimited list or an array. - .. php:method:: setBCC($bcc[, $limit = '']) + .. php:method:: setBCC($bcc[, $limit = '']) - :param mixed $bcc: Comma-delimited string or an array of e-mail addresses - :param int $limit: Maximum number of e-mails to send per batch - :returns: CodeIgniter\\Email\\Email instance (method chaining) - :rtype: CodeIgniter\\Email\\Email + :param mixed $bcc: Comma-delimited string or an array of e-mail addresses + :param int $limit: Maximum number of e-mails to send per batch + :returns: CodeIgniter\\Email\\Email instance (method chaining) + :rtype: CodeIgniter\\Email\\Email - Sets the BCC email address(s). Just like the ``setTo()`` method, can be a single - e-mail, a comma-delimited list or an array. + Sets the BCC email address(s). Just like the ``setTo()`` method, can be a single + e-mail, a comma-delimited list or an array. - If ``$limit`` is set, "batch mode" will be enabled, which will send - the emails to batches, with each batch not exceeding the specified - ``$limit``. + If ``$limit`` is set, "batch mode" will be enabled, which will send + the emails to batches, with each batch not exceeding the specified + ``$limit``. - .. php:method:: setSubject($subject) + .. php:method:: setSubject($subject) - :param string $subject: E-mail subject line - :returns: CodeIgniter\\Email\\Email instance (method chaining) - :rtype: CodeIgniter\\Email\\Email + :param string $subject: E-mail subject line + :returns: CodeIgniter\\Email\\Email instance (method chaining) + :rtype: CodeIgniter\\Email\\Email - Sets the email subject:: + Sets the email subject:: - $email->setSubject('This is my subject'); + $email->setSubject('This is my subject'); - .. php:method:: setMessage($body) + .. php:method:: setMessage($body) - :param string $body: E-mail message body - :returns: CodeIgniter\\Email\\Email instance (method chaining) - :rtype: CodeIgniter\\Email\\Email + :param string $body: E-mail message body + :returns: CodeIgniter\\Email\\Email instance (method chaining) + :rtype: CodeIgniter\\Email\\Email - Sets the e-mail message body:: + Sets the e-mail message body:: - $email->setMessage('This is my message'); + $email->setMessage('This is my message'); - .. php:method:: setAltMessage($str) + .. php:method:: setAltMessage($str) - :param string $str: Alternative e-mail message body - :returns: CodeIgniter\\Email\\Email instance (method chaining) - :rtype: CodeIgniter\\Email\\Email + :param string $str: Alternative e-mail message body + :returns: CodeIgniter\\Email\\Email instance (method chaining) + :rtype: CodeIgniter\\Email\\Email - Sets the alternative e-mail message body:: + Sets the alternative e-mail message body:: - $email->setAltMessage('This is the alternative message'); + $email->setAltMessage('This is the alternative message'); - This is an optional message string which can be used if you send - HTML formatted email. It lets you specify an alternative message - with no HTML formatting which is added to the header string for - people who do not accept HTML email. If you do not set your own - message CodeIgniter will extract the message from your HTML email - and strip the tags. + This is an optional message string which can be used if you send + HTML formatted email. It lets you specify an alternative message + with no HTML formatting which is added to the header string for + people who do not accept HTML email. If you do not set your own + message CodeIgniter will extract the message from your HTML email + and strip the tags. - .. php:method:: setHeader($header, $value) - :noindex: + .. php:method:: setHeader($header, $value) + :noindex: - :param string $header: Header name - :param string $value: Header value - :returns: CodeIgniter\\Email\\Email instance (method chaining) - :rtype: CodeIgniter\\Email\\Email + :param string $header: Header name + :param string $value: Header value + :returns: CodeIgniter\\Email\\Email instance (method chaining) + :rtype: CodeIgniter\\Email\\Email - Appends additional headers to the e-mail:: + Appends additional headers to the e-mail:: - $email->setHeader('Header1', 'Value1'); - $email->setHeader('Header2', 'Value2'); + $email->setHeader('Header1', 'Value1'); + $email->setHeader('Header2', 'Value2'); - .. php:method:: clear($clearAttachments = false) + .. php:method:: clear($clearAttachments = false) - :param bool $clearAttachments: Whether or not to clear attachments - :returns: CodeIgniter\\Email\\Email instance (method chaining) - :rtype: CodeIgniter\\Email\\Email + :param bool $clearAttachments: Whether or not to clear attachments + :returns: CodeIgniter\\Email\\Email instance (method chaining) + :rtype: CodeIgniter\\Email\\Email - Initializes all the email variables to an empty state. This method - is intended for use if you run the email sending method in a loop, - permitting the data to be reset between cycles. + Initializes all the email variables to an empty state. This method + is intended for use if you run the email sending method in a loop, + permitting the data to be reset between cycles. - :: + :: - foreach ($list as $name => $address) - { - $email->clear(); + foreach ($list as $name => $address) + { + $email->clear(); - $email->setTo($address); - $email->setFrom('your@example.com'); - $email->setSubject('Here is your info '.$name); - $email->setMessage('Hi ' . $name . ' Here is the info you requested.'); - $email->send(); - } + $email->setTo($address); + $email->setFrom('your@example.com'); + $email->setSubject('Here is your info '.$name); + $email->setMessage('Hi ' . $name . ' Here is the info you requested.'); + $email->send(); + } - If you set the parameter to true any attachments will be cleared as - well:: + If you set the parameter to true any attachments will be cleared as + well:: - $email->clear(true); + $email->clear(true); - .. php:method:: send($autoClear = true) + .. php:method:: send($autoClear = true) - :param bool $autoClear: Whether to clear message data automatically - :returns: true on success, false on failure - :rtype: bool + :param bool $autoClear: Whether to clear message data automatically + :returns: true on success, false on failure + :rtype: bool - The e-mail sending method. Returns boolean true or false based on - success or failure, enabling it to be used conditionally:: + The e-mail sending method. Returns boolean true or false based on + success or failure, enabling it to be used conditionally:: - if (! $email->send()) - { - // Generate error - } + if (! $email->send()) + { + // Generate error + } - This method will automatically clear all parameters if the request was - successful. To stop this behaviour pass false:: + This method will automatically clear all parameters if the request was + successful. To stop this behaviour pass false:: - if ($email->send(false)) - { - // Parameters won't be cleared - } + if ($email->send(false)) + { + // Parameters won't be cleared + } - .. note:: In order to use the ``printDebugger()`` method, you need - to avoid clearing the email parameters. + .. note:: In order to use the ``printDebugger()`` method, you need + to avoid clearing the email parameters. - .. note:: If ``BCCBatchMode`` is enabled, and there are more than - ``BCCBatchSize`` recipients, this method will always return - boolean ``true``. + .. note:: If ``BCCBatchMode`` is enabled, and there are more than + ``BCCBatchSize`` recipients, this method will always return + boolean ``true``. - .. php:method:: attach($filename[, $disposition = ''[, $newname = null[, $mime = '']]]) + .. php:method:: attach($filename[, $disposition = ''[, $newname = null[, $mime = '']]]) - :param string $filename: File name - :param string $disposition: 'disposition' of the attachment. Most - email clients make their own decision regardless of the MIME - specification used here. https://www.iana.org/assignments/cont-disp/cont-disp.xhtml - :param string $newname: Custom file name to use in the e-mail - :param string $mime: MIME type to use (useful for buffered data) - :returns: CodeIgniter\\Email\\Email instance (method chaining) - :rtype: CodeIgniter\\Email\\Email + :param string $filename: File name + :param string $disposition: 'disposition' of the attachment. Most + email clients make their own decision regardless of the MIME + specification used here. https://www.iana.org/assignments/cont-disp/cont-disp.xhtml + :param string $newname: Custom file name to use in the e-mail + :param string $mime: MIME type to use (useful for buffered data) + :returns: CodeIgniter\\Email\\Email instance (method chaining) + :rtype: CodeIgniter\\Email\\Email - Enables you to send an attachment. Put the file path/name in the first - parameter. For multiple attachments use the method multiple times. - For example:: + Enables you to send an attachment. Put the file path/name in the first + parameter. For multiple attachments use the method multiple times. + For example:: - $email->attach('/path/to/photo1.jpg'); - $email->attach('/path/to/photo2.jpg'); - $email->attach('/path/to/photo3.jpg'); + $email->attach('/path/to/photo1.jpg'); + $email->attach('/path/to/photo2.jpg'); + $email->attach('/path/to/photo3.jpg'); - To use the default disposition (attachment), leave the second parameter blank, - otherwise use a custom disposition:: + To use the default disposition (attachment), leave the second parameter blank, + otherwise use a custom disposition:: - $email->attach('image.jpg', 'inline'); + $email->attach('image.jpg', 'inline'); - You can also use a URL:: + You can also use a URL:: - $email->attach('http://example.com/filename.pdf'); + $email->attach('http://example.com/filename.pdf'); - If you'd like to use a custom file name, you can use the third parameter:: + If you'd like to use a custom file name, you can use the third parameter:: - $email->attach('filename.pdf', 'attachment', 'report.pdf'); + $email->attach('filename.pdf', 'attachment', 'report.pdf'); - If you need to use a buffer string instead of a real - physical - file you can - use the first parameter as buffer, the third parameter as file name and the fourth - parameter as mime-type:: + If you need to use a buffer string instead of a real - physical - file you can + use the first parameter as buffer, the third parameter as file name and the fourth + parameter as mime-type:: - $email->attach($buffer, 'attachment', 'report.pdf', 'application/pdf'); + $email->attach($buffer, 'attachment', 'report.pdf', 'application/pdf'); - .. php:method:: setAttachmentCID($filename) + .. php:method:: setAttachmentCID($filename) - :param string $filename: Existing attachment filename - :returns: Attachment Content-ID or false if not found - :rtype: string + :param string $filename: Existing attachment filename + :returns: Attachment Content-ID or false if not found + :rtype: string - Sets and returns an attachment's Content-ID, which enables your to embed an inline - (picture) attachment into HTML. First parameter must be the already attached file name. - :: + Sets and returns an attachment's Content-ID, which enables your to embed an inline + (picture) attachment into HTML. First parameter must be the already attached file name. + :: - $filename = '/img/photo1.jpg'; - $email->attach($filename); + $filename = '/img/photo1.jpg'; + $email->attach($filename); - foreach ($list as $address) { - $email->setTo($address); - $cid = $email->setAttachmentCID($filename); - $email->setMessage('photo1'); - $email->send(); - } + foreach ($list as $address) { + $email->setTo($address); + $cid = $email->setAttachmentCID($filename); + $email->setMessage('photo1'); + $email->send(); + } - .. note:: Content-ID for each e-mail must be re-created for it to be unique. + .. note:: Content-ID for each e-mail must be re-created for it to be unique. - .. php:method:: printDebugger($include = ['headers', 'subject', 'body']) + .. php:method:: printDebugger($include = ['headers', 'subject', 'body']) - :param array $include: Which parts of the message to print out - :returns: Formatted debug data - :rtype: string + :param array $include: Which parts of the message to print out + :returns: Formatted debug data + :rtype: string - Returns a string containing any server messages, the email headers, and - the email message. Useful for debugging. + Returns a string containing any server messages, the email headers, and + the email message. Useful for debugging. - You can optionally specify which parts of the message should be printed. - Valid options are: **headers**, **subject**, **body**. + You can optionally specify which parts of the message should be printed. + Valid options are: **headers**, **subject**, **body**. - Example:: + Example:: - // You need to pass false while sending in order for the email data - // to not be cleared - if that happens, printDebugger() would have - // nothing to output. - $email->send(false); + // You need to pass false while sending in order for the email data + // to not be cleared - if that happens, printDebugger() would have + // nothing to output. + $email->send(false); - // Will only print the email headers, excluding the message subject and body - $email->printDebugger(['headers']); + // Will only print the email headers, excluding the message subject and body + $email->printDebugger(['headers']); - .. note:: By default, all of the raw data will be printed. + .. note:: By default, all of the raw data will be printed. diff --git a/user_guide_src/source/libraries/encryption.rst b/user_guide_src/source/libraries/encryption.rst index 36185183ca4e..54fb1fb333a7 100644 --- a/user_guide_src/source/libraries/encryption.rst +++ b/user_guide_src/source/libraries/encryption.rst @@ -3,9 +3,9 @@ Encryption Service ################## .. important:: DO NOT use this or any other *encryption* library for - password storage! Passwords must be *hashed* instead, and you - should do that through PHP's `Password Hashing extension - `_. + password storage! Passwords must be *hashed* instead, and you + should do that through PHP's `Password Hashing extension + `_. The Encryption Service provides two-way symmetric (secret key) data encryption. The service will instantiate and/or initialize an @@ -47,11 +47,11 @@ Assuming you have set your starting key (see :ref:`configuration`), encrypting and decrypting data is simple - pass the appropriate string to ``encrypt()`` and/or ``decrypt()`` methods:: - $plainText = 'This is a plain-text message!'; - $ciphertext = $encrypter->encrypt($plainText); + $plainText = 'This is a plain-text message!'; + $ciphertext = $encrypter->encrypt($plainText); - // Outputs: This is a plain-text message! - echo $encrypter->decrypt($ciphertext); + // Outputs: This is a plain-text message! + echo $encrypter->decrypt($ciphertext); And that's it! The Encryption library will do everything necessary for the whole process to be cryptographically secure out-of-the-box. @@ -101,12 +101,12 @@ nor the output of a hashing function, etc. To create a proper key, you can use the Encryption library's ``createKey()`` method. :: - // $key will be assigned a 32-byte (256-bit) random key - $key = \CodeIgniter\Encryption\Encryption::createKey(); + // $key will be assigned a 32-byte (256-bit) random key + $key = \CodeIgniter\Encryption\Encryption::createKey(); - // for the SodiumHandler, you can use either: - $key = sodium_crypto_secretbox_keygen(); - $key = \CodeIgniter\Encryption\Encryption::createKey(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); + // for the SodiumHandler, you can use either: + $key = sodium_crypto_secretbox_keygen(); + $key = \CodeIgniter\Encryption\Encryption::createKey(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); The key can be stored in ``app/Config/Encryption.php``, or you can design a storage mechanism of your own and pass the key dynamically when encrypting/decrypting. @@ -114,7 +114,7 @@ a storage mechanism of your own and pass the key dynamically when encrypting/dec To save your key to your ``app/Config/Encryption.php``, open the file and set:: - public $key = 'YOUR KEY'; + public $key = 'YOUR KEY'; Encoding Keys or Results ------------------------ @@ -124,18 +124,18 @@ is hard to deal with (i.e., a copy-paste may damage it), so you may use ``bin2hex()``, or ``base64_encode`` to work with the key in a more friendly manner. For example:: - // Get a hex-encoded representation of the key: - $encoded = bin2hex(\CodeIgniter\Encryption\Encryption::createKey(32)); + // Get a hex-encoded representation of the key: + $encoded = bin2hex(\CodeIgniter\Encryption\Encryption::createKey(32)); - // Put the same value with hex2bin(), - // so that it is still passed as binary to the library: - $key = hex2bin('your-hex-encoded-key'); + // Put the same value with hex2bin(), + // so that it is still passed as binary to the library: + $key = hex2bin('your-hex-encoded-key'); You might find the same technique useful for the results of encryption:: - // Encrypt some text & make the results text - $encoded = base64_encode($encrypter->encrypt($plaintext)); + // Encrypt some text & make the results text + $encoded = base64_encode($encrypter->encrypt($plaintext)); Using Prefixes in Storing Keys ------------------------------ @@ -147,20 +147,20 @@ intelligently parse the key and still pass a binary string to the library. :: - // In Encryption, you may use - public $key = 'hex2bin:' + // In Encryption, you may use + public $key = 'hex2bin:' - // or - public $key = 'base64:' + // or + public $key = 'base64:' Similarly, you can use these prefixes in your ``.env`` file, too! :: - // For hex2bin - encryption.key = hex2bin: + // For hex2bin + encryption.key = hex2bin: - // or - encryption.key = base64: + // or + encryption.key = base64: Padding ======= @@ -177,11 +177,11 @@ message prior to encryption, and removed after decryption. Padding is configurab ``$blockSize`` property of ``Config\Encryption``. This value should be greater than zero. .. important:: You are advised not to devise your own padding implementation. You must always use - the more secure implementation of a library. Also, passwords should not be padded. Usage of - padding in order to hide the length of a password is not recommended. A client willing to send - a password to a server should hash it instead (even with a single iteration of the hash function). - This ensures that the length of the transmitted data is constant, and that the server doesn't - effortlessly get a copy of the password. + the more secure implementation of a library. Also, passwords should not be padded. Usage of + padding in order to hide the length of a password is not recommended. A client willing to send + a password to a server should hash it instead (even with a single iteration of the hash function). + This ensures that the length of the transmitted data is constant, and that the server doesn't + effortlessly get a copy of the password. Encryption Handler Notes ======================== @@ -209,8 +209,8 @@ a shared-key, such as symmetric encryption, Sodium uses the XSalsa20 algorithm t HMAC-SHA512 for the authentication. .. note:: CodeIgniter's ``SodiumHandler`` uses ``sodium_memzero`` in every encryption or decryption - session. After each session, the message (whether plaintext or ciphertext) and starter key are - wiped out from the buffers. You may need to provide again the key before starting a new session. + session. After each session, the message (whether plaintext or ciphertext) and starter key are + wiped out from the buffers. You may need to provide again the key before starting a new session. Message Length ============== @@ -246,78 +246,78 @@ Class Reference .. php:class:: CodeIgniter\\Encryption\\Encryption - .. php:staticmethod:: createKey([$length = 32]) + .. php:staticmethod:: createKey([$length = 32]) - :param int $length: Output length - :returns: A pseudo-random cryptographic key with the specified length, or ``false`` on failure - :rtype: string + :param int $length: Output length + :returns: A pseudo-random cryptographic key with the specified length, or ``false`` on failure + :rtype: string - Creates a cryptographic key by fetching random data from - the operating system's sources (*i.e.* ``/dev/urandom``). + Creates a cryptographic key by fetching random data from + the operating system's sources (*i.e.* ``/dev/urandom``). - .. php:method:: initialize([Encryption $config = null]) + .. php:method:: initialize([Encryption $config = null]) - :param Config\\Encryption $config: Configuration parameters - :returns: ``CodeIgniter\Encryption\EncrypterInterface`` instance - :rtype: ``CodeIgniter\Encryption\EncrypterInterface`` - :throws: ``CodeIgniter\Encryption\Exceptions\EncryptionException`` + :param Config\\Encryption $config: Configuration parameters + :returns: ``CodeIgniter\Encryption\EncrypterInterface`` instance + :rtype: ``CodeIgniter\Encryption\EncrypterInterface`` + :throws: ``CodeIgniter\Encryption\Exceptions\EncryptionException`` - Initializes (configures) the library to use different settings. + Initializes (configures) the library to use different settings. - Example:: + Example:: - $encrypter = $encryption->initialize(['cipher' => '3des']); + $encrypter = $encryption->initialize(['cipher' => '3des']); - Please refer to the :ref:`configuration` section for detailed info. + Please refer to the :ref:`configuration` section for detailed info. .. php:interface:: CodeIgniter\\Encryption\\EncrypterInterface - .. php:method:: encrypt($data[, $params = null]) + .. php:method:: encrypt($data[, $params = null]) - :param string $data: Data to encrypt - :param array|string|null $params: Configuration parameters (key) - :returns: Encrypted data - :rtype: string - :throws: ``CodeIgniter\Encryption\Exceptions\EncryptionException`` + :param string $data: Data to encrypt + :param array|string|null $params: Configuration parameters (key) + :returns: Encrypted data + :rtype: string + :throws: ``CodeIgniter\Encryption\Exceptions\EncryptionException`` - Encrypts the input data and returns its ciphertext. + Encrypts the input data and returns its ciphertext. - If you pass parameters as the second argument, the ``key`` element - will be used as the starting key for this operation if ``$params`` - is an array; or the starting key may be passed as a string. + If you pass parameters as the second argument, the ``key`` element + will be used as the starting key for this operation if ``$params`` + is an array; or the starting key may be passed as a string. - If you are using the SodiumHandler and want to pass a different ``blockSize`` - on runtime, pass the ``blockSize`` key in the ``$params`` array. + If you are using the SodiumHandler and want to pass a different ``blockSize`` + on runtime, pass the ``blockSize`` key in the ``$params`` array. - Examples:: + Examples:: - $ciphertext = $encrypter->encrypt('My secret message'); - $ciphertext = $encrypter->encrypt('My secret message', ['key' => 'New secret key']); - $ciphertext = $encrypter->encrypt('My secret message', ['key' => 'New secret key', 'blockSize' => 32]); - $ciphertext = $encrypter->encrypt('My secret message', 'New secret key'); - $ciphertext = $encrypter->encrypt('My secret message', ['blockSize' => 32]); + $ciphertext = $encrypter->encrypt('My secret message'); + $ciphertext = $encrypter->encrypt('My secret message', ['key' => 'New secret key']); + $ciphertext = $encrypter->encrypt('My secret message', ['key' => 'New secret key', 'blockSize' => 32]); + $ciphertext = $encrypter->encrypt('My secret message', 'New secret key'); + $ciphertext = $encrypter->encrypt('My secret message', ['blockSize' => 32]); - .. php:method:: decrypt($data[, $params = null]) + .. php:method:: decrypt($data[, $params = null]) - :param string $data: Data to decrypt - :param array|string|null $params: Configuration parameters (key) - :returns: Decrypted data - :rtype: string - :throws: ``CodeIgniter\Encryption\Exceptions\EncryptionException`` + :param string $data: Data to decrypt + :param array|string|null $params: Configuration parameters (key) + :returns: Decrypted data + :rtype: string + :throws: ``CodeIgniter\Encryption\Exceptions\EncryptionException`` - Decrypts the input data and returns it in plain-text. + Decrypts the input data and returns it in plain-text. - If you pass parameters as the second argument, the ``key`` element - will be used as the starting key for this operation if ``$params`` - is an array; or the starting key may be passed as a string. + If you pass parameters as the second argument, the ``key`` element + will be used as the starting key for this operation if ``$params`` + is an array; or the starting key may be passed as a string. - If you are using the SodiumHandler and want to pass a different ``blockSize`` - on runtime, pass the ``blockSize`` key in the ``$params`` array. + If you are using the SodiumHandler and want to pass a different ``blockSize`` + on runtime, pass the ``blockSize`` key in the ``$params`` array. - Examples:: + Examples:: - echo $encrypter->decrypt($ciphertext); - echo $encrypter->decrypt($ciphertext, ['key' => 'New secret key']); - echo $encrypter->decrypt($ciphertext, ['key' => 'New secret key', 'blockSize' => 32]); - echo $encrypter->decrypt($ciphertext, 'New secret key'); - echo $encrypter->decrypt($ciphertext, ['blockSize' => 32]); + echo $encrypter->decrypt($ciphertext); + echo $encrypter->decrypt($ciphertext, ['key' => 'New secret key']); + echo $encrypter->decrypt($ciphertext, ['key' => 'New secret key', 'blockSize' => 32]); + echo $encrypter->decrypt($ciphertext, 'New secret key'); + echo $encrypter->decrypt($ciphertext, ['blockSize' => 32]); diff --git a/user_guide_src/source/libraries/images.rst b/user_guide_src/source/libraries/images.rst index d22f3eba9af6..bd71be9657f2 100644 --- a/user_guide_src/source/libraries/images.rst +++ b/user_guide_src/source/libraries/images.rst @@ -24,7 +24,7 @@ Initializing the Class Like most other classes in CodeIgniter, the image class is initialized in your controller by calling the Services class:: - $image = \Config\Services::image(); + $image = \Config\Services::image(); You can pass the alias for the image library you wish to use into the Service function:: @@ -52,10 +52,10 @@ identical. You will set some preferences corresponding to the action you intend to perform, then call one of the available processing functions. For example, to create an image thumbnail you'll do this:: - $image = \Config\Services::image() - ->withFile('/path/to/image/mypic.jpg') - ->fit(100, 100, 'center') - ->save('/path/to/image/mypic_thumb.jpg'); + $image = \Config\Services::image() + ->withFile('/path/to/image/mypic.jpg') + ->fit(100, 100, 'center') + ->save('/path/to/image/mypic_thumb.jpg'); The above code tells the library to look for an image called *mypic.jpg* located in the source_image folder, then create a @@ -69,25 +69,25 @@ needed before saving. The original image is left untouched, and a new image is used and passed through each method, applying the results on top of the previous results:: - $image = \Config\Services::image() - ->withFile('/path/to/image/mypic.jpg') - ->reorient() - ->rotate(90) - ->crop(100, 100, 0, 0) - ->save('/path/to/image/mypic_thumb.jpg'); + $image = \Config\Services::image() + ->withFile('/path/to/image/mypic.jpg') + ->reorient() + ->rotate(90) + ->crop(100, 100, 0, 0) + ->save('/path/to/image/mypic_thumb.jpg'); This example would take the same image and first fix any mobile phone orientation issues, rotate the image by 90 degrees, and then crop the result into a 100x100 pixel image, starting at the top left corner. The result would be saved as the thumbnail. .. note:: In order for the image class to be allowed to do any - processing, the folder containing the image files must have write - permissions. + processing, the folder containing the image files must have write + permissions. .. note:: Image processing can require a considerable amount of server - memory for some operations. If you are experiencing out of memory errors - while processing images you may need to limit their maximum size, and/or - adjust PHP memory limits. + memory for some operations. If you are experiencing out of memory errors + while processing images you may need to limit their maximum size, and/or + adjust PHP memory limits. Image Quality ============= @@ -96,20 +96,20 @@ Image Quality quality. Values range from 0 to 100 with 90 being the framework default. This parameter only applies to JPEG images and will be ignored otherwise:: - $image = \Config\Services::image() - ->withFile('/path/to/image/mypic.jpg') - // processing methods - ->save('/path/to/image/my_low_quality_pic.jpg', 10); + $image = \Config\Services::image() + ->withFile('/path/to/image/mypic.jpg') + // processing methods + ->save('/path/to/image/my_low_quality_pic.jpg', 10); .. note:: Higher quality will result in larger file sizes. See also https://www.php.net/manual/en/function.imagejpeg.php If you are only interested in changing the image quality without doing any processing. You will need to include the image resource or you will end up with an exact copy:: - $image = \Config\Services::image() - ->withFile('/path/to/image/mypic.jpg') - ->withResource() - ->save('/path/to/image/my_low_quality_pic.jpg', 10); + $image = \Config\Services::image() + ->withFile('/path/to/image/mypic.jpg') + ->withResource() + ->save('/path/to/image/my_low_quality_pic.jpg', 10); Processing Methods ================== @@ -130,14 +130,14 @@ If they fail they will throw a ``CodeIgniter\Images\ImageException`` that contai the error message. A good practice is to catch the exceptions, showing an error upon failure, like this:: - try { + try { $image = \Config\Services::image() ->withFile('/path/to/image/mypic.jpg') ->fit(100, 100, 'center') ->save('/path/to/image/mypic_thumb.jpg'); - } catch (CodeIgniter\Images\ImageException $e) { - echo $e->getMessage(); - } + } catch (CodeIgniter\Images\ImageException $e) { + echo $e->getMessage(); + } Cropping Images --------------- @@ -158,34 +158,34 @@ To take a 50x50 pixel square out of the center of an image, you would need to fi offset values:: $info = \Config\Services::image('imagick') - ->withFile('/path/to/image/mypic.jpg') - ->getFile() - ->getProperties(true); + ->withFile('/path/to/image/mypic.jpg') + ->getFile() + ->getProperties(true); $xOffset = ($info['width'] / 2) - 25; $yOffset = ($info['height'] / 2) - 25; \Config\Services::image('imagick') - ->withFile('/path/to/image/mypic.jpg') - ->crop(50, 50, $xOffset, $yOffset) - ->save('/path/to/new/image.jpg'); + ->withFile('/path/to/image/mypic.jpg') + ->crop(50, 50, $xOffset, $yOffset) + ->save('/path/to/new/image.jpg'); Converting Images ----------------- The ``convert()`` method changes the library's internal indicator for the desired file format. This doesn't touch the actual image resource, but indicates to ``save()`` what format to use:: - convert(int $imageType) + convert(int $imageType) - **$imageType** is one of PHP's image type constants (see for example https://www.php.net/manual/en/function.image-type-to-mime-type.php):: - \Config\Services::image() - ->withFile('/path/to/image/mypic.jpg') - ->convert(IMAGETYPE_PNG) - ->save('/path/to/new/image.png'); + \Config\Services::image() + ->withFile('/path/to/image/mypic.jpg') + ->convert(IMAGETYPE_PNG) + ->save('/path/to/new/image.png'); .. note:: ImageMagick already saves files in the type - indicated by their extension, ignoring **$imageType** + indicated by their extension, ignoring **$imageType** Fitting Images -------------- @@ -206,10 +206,10 @@ The ``fit()`` method aims to help simplify cropping a portion of an image in a " This provides a much simpler way to crop that will always maintain the aspect ratio:: - \Config\Services::image('imagick') - ->withFile('/path/to/image/mypic.jpg') - ->fit(100, 150, 'left') - ->save('/path/to/new/image.jpg'); + \Config\Services::image('imagick') + ->withFile('/path/to/image/mypic.jpg') + ->fit(100, 150, 'left') + ->save('/path/to/new/image.jpg'); Flattening Images ----------------- @@ -228,15 +228,15 @@ The ``flatten()`` method aims to add a background color behind transparent image :: - \Config\Services::image('imagick') - ->withFile('/path/to/image/mypic.png') - ->flatten() - ->save('/path/to/new/image.jpg'); + \Config\Services::image('imagick') + ->withFile('/path/to/image/mypic.png') + ->flatten() + ->save('/path/to/new/image.jpg'); - \Config\Services::image('imagick') - ->withFile('/path/to/image/mypic.png') - ->flatten(25,25,112) - ->save('/path/to/new/image.jpg'); + \Config\Services::image('imagick') + ->withFile('/path/to/image/mypic.png') + ->flatten(25,25,112) + ->save('/path/to/new/image.jpg'); Flipping Images --------------- @@ -249,17 +249,17 @@ Images can be flipped along either their horizontal or vertical axis:: :: - \Config\Services::image('imagick') - ->withFile('/path/to/image/mypic.jpg') - ->flip('horizontal') - ->save('/path/to/new/image.jpg'); + \Config\Services::image('imagick') + ->withFile('/path/to/image/mypic.jpg') + ->flip('horizontal') + ->save('/path/to/new/image.jpg'); Resizing Images --------------- Images can be resized to fit any dimension you require with the resize() method:: - resize(int $width, int $height, bool $maintainRatio = false, string $masterDim = 'auto') + resize(int $width, int $height, bool $maintainRatio = false, string $masterDim = 'auto') - **$width** is the desired width of the new image in pixels - **$height** is the desired height of the new image in pixels @@ -272,22 +272,22 @@ while the other dimension will be altered to match the original image's aspect r :: - \Config\Services::image('imagick') - ->withFile('/path/to/image/mypic.jpg') - ->resize(200, 100, true, 'height') - ->save('/path/to/new/image.jpg'); + \Config\Services::image('imagick') + ->withFile('/path/to/image/mypic.jpg') + ->resize(200, 100, true, 'height') + ->save('/path/to/new/image.jpg'); Rotating Images --------------- The rotate() method allows you to rotate an image in 90 degree increments:: - rotate(float $angle) + rotate(float $angle) - **$angle** is the number of degrees to rotate. One of '90', '180', '270'. .. note:: While the $angle parameter accepts a float, it will convert it to an integer during the process. - If the value is any other than the three values listed above, it will throw a CodeIgniter\Images\ImageException. + If the value is any other than the three values listed above, it will throw a CodeIgniter\Images\ImageException. Adding a Text Watermark ----------------------- @@ -298,36 +298,36 @@ products. :: - text(string $text, array $options = []) + text(string $text, array $options = []) The first parameter is the string of text that you wish to display. The second parameter is an array of options that allow you to specify how the text should be displayed:: - \Config\Services::image('imagick') - ->withFile('/path/to/image/mypic.jpg') - ->text('Copyright 2017 My Photo Co', [ - 'color' => '#fff', - 'opacity' => 0.5, - 'withShadow' => true, - 'hAlign' => 'center', - 'vAlign' => 'bottom', - 'fontSize' => 20 - ]) - ->save('/path/to/new/image.jpg'); + \Config\Services::image('imagick') + ->withFile('/path/to/image/mypic.jpg') + ->text('Copyright 2017 My Photo Co', [ + 'color' => '#fff', + 'opacity' => 0.5, + 'withShadow' => true, + 'hAlign' => 'center', + 'vAlign' => 'bottom', + 'fontSize' => 20 + ]) + ->save('/path/to/new/image.jpg'); The possible options that are recognized are as follows: - color Text Color (hex number), i.e., #ff0000 -- opacity A number between 0 and 1 that represents the opacity of the text. -- withShadow Boolean value whether to display a shadow or not. +- opacity A number between 0 and 1 that represents the opacity of the text. +- withShadow Boolean value whether to display a shadow or not. - shadowColor Color of the shadow (hex number) -- shadowOffset How many pixels to offset the shadow. Applies to both the vertical and horizontal values. +- shadowOffset How many pixels to offset the shadow. Applies to both the vertical and horizontal values. - hAlign Horizontal alignment: left, center, right - vAlign Vertical alignment: top, middle, bottom -- hOffset Additional offset on the x axis, in pixels -- vOffset Additional offset on the y axis, in pixels -- fontPath The full server path to the TTF font you wish to use. System font will be used if none is given. -- fontSize The font size to use. When using the GD handler with the system font, valid values are between 1-5. +- hOffset Additional offset on the x axis, in pixels +- vOffset Additional offset on the y axis, in pixels +- fontPath The full server path to the TTF font you wish to use. System font will be used if none is given. +- fontSize The font size to use. When using the GD handler with the system font, valid values are between 1-5. .. note:: The ImageMagick driver does not recognize full server path for fontPath. Instead, simply provide the - name of one of the installed system fonts that you wish to use, i.e., Calibri. + name of one of the installed system fonts that you wish to use, i.e., Calibri. diff --git a/user_guide_src/source/libraries/publisher.rst b/user_guide_src/source/libraries/publisher.rst index dc2ec08347a8..fd75c09e99e4 100644 --- a/user_guide_src/source/libraries/publisher.rst +++ b/user_guide_src/source/libraries/publisher.rst @@ -15,7 +15,7 @@ Loading the Library Because Publisher instances are specific to their source and destination this library is not available through ``Services`` but should be instantiated or extended directly. E.g. - $publisher = new \CodeIgniter\Publisher\Publisher(); + $publisher = new \CodeIgniter\Publisher\Publisher(); ***************** Concept and Usage @@ -38,41 +38,41 @@ On Demand Access ``Publisher`` directly by instantiating a new instance of the class:: - $publisher = new \CodeIgniter\Publisher\Publisher(); + $publisher = new \CodeIgniter\Publisher\Publisher(); By default the source and destination will be set to ``ROOTPATH`` and ``FCPATH`` respectively, giving ``Publisher`` easy access to take any file from your project and make it web-accessible. Alternatively you may pass a new source or source and destination into the constructor:: - use CodeIgniter\Publisher\Publisher; - - $vendorPublisher = new Publisher(ROOTPATH . 'vendor'); - $filterPublisher = new Publisher('/path/to/module/Filters', APPPATH . 'Filters'); + use CodeIgniter\Publisher\Publisher; + + $vendorPublisher = new Publisher(ROOTPATH . 'vendor'); + $filterPublisher = new Publisher('/path/to/module/Filters', APPPATH . 'Filters'); - // Once the source and destination are set you may start adding relative input files - $frameworkPublisher = new Publisher(ROOTPATH . 'vendor/codeigniter4/codeigniter4'); + // Once the source and destination are set you may start adding relative input files + $frameworkPublisher = new Publisher(ROOTPATH . 'vendor/codeigniter4/codeigniter4'); - // All "path" commands are relative to $source - $frameworkPublisher->addPath('app/Config/Cookie.php'); + // All "path" commands are relative to $source + $frameworkPublisher->addPath('app/Config/Cookie.php'); - // You may also add from outside the source, but the files will not be merged into subdirectories - $frameworkPublisher->addFiles([ - '/opt/mail/susan', - '/opt/mail/ubuntu', - ]); - $frameworkPublisher->addDirectory(SUPPORTPATH . 'Images'); + // You may also add from outside the source, but the files will not be merged into subdirectories + $frameworkPublisher->addFiles([ + '/opt/mail/susan', + '/opt/mail/ubuntu', + ]); + $frameworkPublisher->addDirectory(SUPPORTPATH . 'Images'); Once all the files are staged use one of the output commands (**copy()** or **merge()**) to process the staged files to their destination(s):: - // Place all files into $destination - $frameworkPublisher->copy(); + // Place all files into $destination + $frameworkPublisher->copy(); - // Place all files into $destination, overwriting existing files - $frameworkPublisher->copy(true); + // Place all files into $destination, overwriting existing files + $frameworkPublisher->copy(true); - // Place files into their relative $destination directories, overwriting and saving the boolean result - $result = $frameworkPublisher->merge(true); + // Place files into their relative $destination directories, overwriting and saving the boolean result + $result = $frameworkPublisher->merge(true); See the :ref:`reference` for a full description of available methods. @@ -82,27 +82,27 @@ Automation and Discovery You may have regular publication tasks embedded as part of your application deployment or upkeep. ``Publisher`` leverages the powerful ``Autoloader`` to locate any child classes primed for publication:: - use CodeIgniter\CLI\CLI; - use CodeIgniter\Publisher\Publisher; - - foreach (Publisher::discover() as $publisher) - { - $result = $publisher->publish(); + use CodeIgniter\CLI\CLI; + use CodeIgniter\Publisher\Publisher; + + foreach (Publisher::discover() as $publisher) + { + $result = $publisher->publish(); - if ($result === false) - { - CLI::error(get_class($publisher) . ' failed to publish!', 'red'); - } - } + if ($result === false) + { + CLI::error(get_class($publisher) . ' failed to publish!', 'red'); + } + } By default ``discover()`` will search for the "Publishers" directory across all namespaces, but you may specify a different directory and it will return any child classes found:: - $memePublishers = Publisher::discover('CatGIFs'); + $memePublishers = Publisher::discover('CatGIFs'); Most of the time you will not need to handle your own discovery, just use the provided "publish" command:: - > php spark publish + > php spark publish By default on your class extension ``publish()`` will add all files from your ``$source`` and merge them out to your destination, overwriting on collision. @@ -134,40 +134,40 @@ You want to display a "photo of the day" image on your homepage. You have a feed need to get the actual file into a browsable location in your project at **public/images/daily_photo.jpg**. You can set up :doc:`Custom Command ` to run daily that will handle this for you:: - addPath('daily_photo.jpg')->copy(true); // `true` to enable overwrites - } - catch (Throwable $e) - { - $this->showError($e); - } - } - } + try + { + $publisher->addPath('daily_photo.jpg')->copy(true); // `true` to enable overwrites + } + catch (Throwable $e) + { + $this->showError($e); + } + } + } Now running ``spark publish:daily`` will keep your homepage's image up-to-date. What if the photo is coming from an external API? You can use ``addUri()`` in place of ``addPath()`` to download the remote resource and publish it out instead:: - $publisher->addUri('https://example.com/feeds/daily_photo.jpg')->copy(true); + $publisher->addUri('https://example.com/feeds/daily_photo.jpg')->copy(true); Asset Dependencies Example ========================== @@ -176,82 +176,82 @@ You want to integrate the frontend library "Bootstrap" into your project, but th to keep up with. You can create a publication definition in your project to sync frontend assets by extending ``Publisher`` in your project. So **app/Publishers/BootstrapPublisher.php** might look like this:: - addPath('dist') - - // Indicate we only want the minimized versions - ->retainPattern('*.min.*') - - // Merge-and-replace to retain the original directory structure - ->merge(true); - } - } + addPath('dist') + + // Indicate we only want the minimized versions + ->retainPattern('*.min.*') + + // Merge-and-replace to retain the original directory structure + ->merge(true); + } + } Now add the dependency via Composer and call ``spark publish`` to run the publication:: - > composer require twbs/bootstrap - > php spark publish + > composer require twbs/bootstrap + > php spark publish ... and you'll end up with something like this:: - public/.htaccess - public/favicon.ico - public/index.php - public/robots.txt - public/ - bootstrap/ - css/ - bootstrap.min.css - bootstrap-utilities.min.css.map - bootstrap-grid.min.css - bootstrap.rtl.min.css - bootstrap.min.css.map - bootstrap-reboot.min.css - bootstrap-utilities.min.css - bootstrap-reboot.rtl.min.css - bootstrap-grid.min.css.map - js/ - bootstrap.esm.min.js - bootstrap.bundle.min.js.map - bootstrap.bundle.min.js - bootstrap.min.js - bootstrap.esm.min.js.map - bootstrap.min.js.map + public/.htaccess + public/favicon.ico + public/index.php + public/robots.txt + public/ + bootstrap/ + css/ + bootstrap.min.css + bootstrap-utilities.min.css.map + bootstrap-grid.min.css + bootstrap.rtl.min.css + bootstrap.min.css.map + bootstrap-reboot.min.css + bootstrap-utilities.min.css + bootstrap-reboot.rtl.min.css + bootstrap-grid.min.css.map + js/ + bootstrap.esm.min.js + bootstrap.bundle.min.js.map + bootstrap.bundle.min.js + bootstrap.min.js + bootstrap.esm.min.js.map + bootstrap.min.js.map Module Deployment Example ========================= @@ -260,59 +260,59 @@ You want to allow developers using your popular authentication module the abilit of your Migration, Controller, and Model. You can create your own module "publish" command to inject these components into an application for use:: - getNamespace('Math\\Auth'); - - $publisher = new Publisher($source, APPATH); - - try - { - // Add only the desired components - $publisher->addPaths([ - 'Controllers', - 'Database/Migrations', - 'Models', - ])->merge(false); // Be careful not to overwrite anything - } - catch (Throwable $e) - { - $this->showError($e); - return; - } - - // If publication succeeded then update namespaces - foreach ($publisher->getPublished() as $file) - { - // Replace the namespace - $contents = file_get_contents($file); - $contents = str_replace('namespace Math\\Auth', 'namespace ' . APP_NAMESPACE, ); - file_put_contents($file, $contents); - } - } - } + getNamespace('Math\\Auth'); + + $publisher = new Publisher($source, APPATH); + + try + { + // Add only the desired components + $publisher->addPaths([ + 'Controllers', + 'Database/Migrations', + 'Models', + ])->merge(false); // Be careful not to overwrite anything + } + catch (Throwable $e) + { + $this->showError($e); + return; + } + + // If publication succeeded then update namespaces + foreach ($publisher->getPublished() as $file) + { + // Replace the namespace + $contents = file_get_contents($file); + $contents = str_replace('namespace Math\\Auth', 'namespace ' . APP_NAMESPACE, ); + file_put_contents($file, $contents); + } + } + } Now when your module users run ``php spark auth:publish`` they will have the following added to their project:: - app/Controllers/AuthController.php - app/Database/Migrations/2017-11-20-223112_create_auth_tables.php.php - app/Models/LoginModel.php - app/Models/UserModel.php + app/Controllers/AuthController.php + app/Database/Migrations/2017-11-20-223112_create_auth_tables.php.php + app/Models/LoginModel.php + app/Models/UserModel.php .. _reference: @@ -361,7 +361,7 @@ Downloads the contents of a URI using ``CURLRequest`` into the scratch workspace file to the list. .. note:: The CURL request made is a simple ``GET`` and uses the response body for the file contents. Some - remote files may need a custom request to be handled properly. + remote files may need a custom request to be handled properly. Outputting Files ================ @@ -380,14 +380,14 @@ to overwrite when there is already an existing file. Returns success or failure, and ``getErrors()`` to troubleshoot failures. Be mindful of duplicate basename collisions, for example:: - $publisher = new Publisher('/home/source', '/home/destination'); - $publisher->addPaths([ - 'pencil/lead.png', - 'metal/lead.png', - ]); + $publisher = new Publisher('/home/source', '/home/destination'); + $publisher->addPaths([ + 'pencil/lead.png', + 'metal/lead.png', + ]); - // This is bad! Only one file will remain at /home/destination/lead.png - $publisher->copy(true); + // This is bad! Only one file will remain at /home/destination/lead.png + $publisher->copy(true); **merge(bool $replace = true): bool** @@ -400,11 +400,11 @@ affect other files in the destination. Returns success or failure, use ``getPubl Example:: - $publisher = new Publisher('/home/source', '/home/destination'); - $publisher->addPaths([ - 'pencil/lead.png', - 'metal/lead.png', - ]); + $publisher = new Publisher('/home/source', '/home/destination'); + $publisher->addPaths([ + 'pencil/lead.png', + 'metal/lead.png', + ]); - // Results in "/home/destination/pencil/lead.png" and "/home/destination/metal/lead.png" - $publisher->merge(); + // Results in "/home/destination/pencil/lead.png" and "/home/destination/metal/lead.png" + $publisher->merge(); diff --git a/user_guide_src/source/libraries/sessions.rst b/user_guide_src/source/libraries/sessions.rst index 1472afa4000c..b2efd60b7025 100644 --- a/user_guide_src/source/libraries/sessions.rst +++ b/user_guide_src/source/libraries/sessions.rst @@ -23,7 +23,7 @@ class should be magically initialized. To access and initialize the session:: - $session = \Config\Services::session($config); + $session = \Config\Services::session($config); The ``$config`` parameter is optional - your application configuration. If not provided, the services register will instantiate your default @@ -31,14 +31,14 @@ one. Once loaded, the Sessions library object will be available using:: - $session + $session Alternatively, you can use the helper function that will use the default configuration options. This version is a little friendlier to read, but does not take any configuration options. :: - $session = session(); + $session = session(); How do Sessions work? ===================== @@ -58,7 +58,7 @@ data, but the process of reading, writing, and updating a session is automatic. .. note:: Under CLI, the Session library will automatically halt itself, - as this is a concept based entirely on the HTTP protocol. + as this is a concept based entirely on the HTTP protocol. A note about concurrency ------------------------ @@ -117,45 +117,45 @@ Retrieving Session Data Any piece of information from the session array is available through the ``$_SESSION`` superglobal:: - $_SESSION['item'] + $_SESSION['item'] Or through the conventional accessor method:: - $session->get('item'); + $session->get('item'); Or through the magic getter:: - $session->item + $session->item Or even through the session helper method:: - session('item'); + session('item'); Where ``item`` is the array key corresponding to the item you wish to fetch. For example, to assign a previously stored 'name' item to the ``$name`` variable, you will do this:: - $name = $_SESSION['name']; + $name = $_SESSION['name']; - // or: + // or: - $name = $session->name + $name = $session->name - // or: + // or: - $name = $session->get('name'); + $name = $session->get('name'); .. note:: The ``get()`` method returns null if the item you are trying - to access does not exist. + to access does not exist. If you want to retrieve all of the existing userdata, you can simply omit the item key (magic getter only works for single property values):: - $_SESSION + $_SESSION - // or: + // or: - $session->get(); + $session->get(); Adding Session Data =================== @@ -172,34 +172,34 @@ The former userdata method is deprecated, but you can pass an array containing your new session data to the ``set()`` method:: - $session->set($array); + $session->set($array); Where ``$array`` is an associative array containing your new data. Here's an example:: - $newdata = [ - 'username' => 'johndoe', - 'email' => 'johndoe@some-site.com', - 'logged_in' => true, - ]; + $newdata = [ + 'username' => 'johndoe', + 'email' => 'johndoe@some-site.com', + 'logged_in' => true, + ]; - $session->set($newdata); + $session->set($newdata); If you want to add session data one value at a time, ``set()`` also supports this syntax:: - $session->set('some_name', 'some_value'); + $session->set('some_name', 'some_value'); If you want to verify that a session value exists, simply check with ``isset()``:: - // returns false if the 'some_name' item doesn't exist or is null, - // true otherwise: - isset($_SESSION['some_name']) + // returns false if the 'some_name' item doesn't exist or is null, + // true otherwise: + isset($_SESSION['some_name']) Or you can call ``has()``:: - $session->has('some_name'); + $session->has('some_name'); Pushing new value to session data ================================= @@ -215,26 +215,26 @@ Removing Session Data Just as with any other variable, unsetting a value in ``$_SESSION`` can be done through ``unset()``:: - unset($_SESSION['some_name']); + unset($_SESSION['some_name']); - // or multiple values: + // or multiple values: - unset( - $_SESSION['some_name'], - $_SESSION['another_name'] - ); + unset( + $_SESSION['some_name'], + $_SESSION['another_name'] + ); Also, just as ``set()`` can be used to add information to a session, ``remove()`` can be used to remove it, by passing the session key. For example, if you wanted to remove 'some_name' from your session data array:: - $session->remove('some_name'); + $session->remove('some_name'); This method also accepts an array of item keys to unset:: - $array_items = ['username', 'email']; - $session->remove($array_items); + $array_items = ['username', 'email']; + $session->remove($array_items); Flashdata ========= @@ -250,21 +250,21 @@ managed inside the CodeIgniter session handler. To mark an existing item as "flashdata":: - $session->markAsFlashdata('item'); + $session->markAsFlashdata('item'); If you want to mark multiple items as flashdata, simply pass the keys as an array:: - $session->markAsFlashdata(['item', 'item2']); + $session->markAsFlashdata(['item', 'item2']); To add flashdata:: - $_SESSION['item'] = 'value'; - $session->markAsFlashdata('item'); + $_SESSION['item'] = 'value'; + $session->markAsFlashdata('item'); Or alternatively, using the ``setFlashdata()`` method:: - $session->setFlashdata('item', 'value'); + $session->setFlashdata('item', 'value'); You can also pass an array to ``setFlashdata()``, in the same manner as ``set()``. @@ -272,23 +272,23 @@ You can also pass an array to ``setFlashdata()``, in the same manner as Reading flashdata variables is the same as reading regular session data through ``$_SESSION``:: - $_SESSION['item'] + $_SESSION['item'] .. important:: The ``get()`` method WILL return flashdata items when - retrieving a single item by key. It will not return flashdata when - grabbing all userdata from the session, however. + retrieving a single item by key. It will not return flashdata when + grabbing all userdata from the session, however. However, if you want to be sure that you're reading "flashdata" (and not any other kind), you can also use the ``getFlashdata()`` method:: - $session->getFlashdata('item'); + $session->getFlashdata('item'); Or to get an array with all flashdata, simply omit the key parameter:: - $session->getFlashdata(); + $session->getFlashdata(); .. note:: The ``getFlashdata()`` method returns null if the item cannot be - found. + found. If you find that you need to preserve a flashdata variable through an additional request, you can do so using the ``keepFlashdata()`` method. @@ -296,8 +296,8 @@ You can either pass a single item or an array of flashdata items to keep. :: - $session->keepFlashdata('item'); - $session->keepFlashdata(['item1', 'item2', 'item3']); + $session->keepFlashdata('item'); + $session->keepFlashdata(['item1', 'item2', 'item3']); Tempdata ======== @@ -312,71 +312,71 @@ CodeIgniter session handler. To mark an existing item as "tempdata", simply pass its key and expiry time (in seconds!) to the ``markAsTempdata()`` method:: - // 'item' will be erased after 300 seconds - $session->markAsTempdata('item', 300); + // 'item' will be erased after 300 seconds + $session->markAsTempdata('item', 300); You can mark multiple items as tempdata in two ways, depending on whether you want them all to have the same expiry time or not:: - // Both 'item' and 'item2' will expire after 300 seconds - $session->markAsTempdata(['item', 'item2'], 300); + // Both 'item' and 'item2' will expire after 300 seconds + $session->markAsTempdata(['item', 'item2'], 300); - // 'item' will be erased after 300 seconds, while 'item2' - // will do so after only 240 seconds - $session->markAsTempdata([ - 'item' => 300, - 'item2' => 240, - ]); + // 'item' will be erased after 300 seconds, while 'item2' + // will do so after only 240 seconds + $session->markAsTempdata([ + 'item' => 300, + 'item2' => 240, + ]); To add tempdata:: - $_SESSION['item'] = 'value'; - $session->markAsTempdata('item', 300); // Expire in 5 minutes + $_SESSION['item'] = 'value'; + $session->markAsTempdata('item', 300); // Expire in 5 minutes Or alternatively, using the ``setTempdata()`` method:: - $session->setTempdata('item', 'value', 300); + $session->setTempdata('item', 'value', 300); You can also pass an array to ``setTempdata()``:: - $tempdata = ['newuser' => true, 'message' => 'Thanks for joining!']; - $session->setTempdata($tempdata, null, $expire); + $tempdata = ['newuser' => true, 'message' => 'Thanks for joining!']; + $session->setTempdata($tempdata, null, $expire); .. note:: If the expiration is omitted or set to 0, the default - time-to-live value of 300 seconds (or 5 minutes) will be used. + time-to-live value of 300 seconds (or 5 minutes) will be used. To read a tempdata variable, again you can just access it through the ``$_SESSION`` superglobal array:: - $_SESSION['item'] + $_SESSION['item'] .. important:: The ``get()`` method WILL return tempdata items when - retrieving a single item by key. It will not return tempdata when - grabbing all userdata from the session, however. + retrieving a single item by key. It will not return tempdata when + grabbing all userdata from the session, however. Or if you want to be sure that you're reading "tempdata" (and not any other kind), you can also use the ``getTempdata()`` method:: - $session->getTempdata('item'); + $session->getTempdata('item'); And of course, if you want to retrieve all existing tempdata:: - $session->getTempdata(); + $session->getTempdata(); .. note:: The ``getTempdata()`` method returns null if the item cannot be - found. + found. If you need to remove a tempdata value before it expires, you can directly unset it from the ``$_SESSION`` array:: - unset($_SESSION['item']); + unset($_SESSION['item']); However, this won't remove the marker that makes this specific item to be tempdata (it will be invalidated on the next HTTP request), so if you intend to reuse that same key in the same request, you'd want to use ``removeTempdata()``:: - $session->removeTempdata('item'); + $session->removeTempdata('item'); Destroying a Session ==================== @@ -386,16 +386,16 @@ simply use either PHP's `session_destroy() function, or the library's ``destroy()`` method. Both will work in exactly the same way:: - session_destroy(); + session_destroy(); - // or + // or - $session->destroy(); + $session->destroy(); .. note:: This must be the last session-related operation that you do - during the same request. All session data (including flashdata and - tempdata) will be destroyed permanently and functions will be - unusable during the same request after you destroy the session. + during the same request. All session data (including flashdata and + tempdata) will be destroyed permanently and functions will be + unusable during the same request after you destroy the session. You may also use the ``stop()`` method to completely kill the session by removing the old session_id, destroying all data, and destroying @@ -452,11 +452,11 @@ Preference Default Opti ============================== ============================================ ================================================= ============================================================================================ .. note:: As a last resort, the Session library will try to fetch PHP's - session related INI settings, as well as legacy CI settings such as - 'sess_expire_on_close' when any of the above is not configured. - However, you should never rely on this behavior as it can cause - unexpected results or be changed in the future. Please configure - everything properly. + session related INI settings, as well as legacy CI settings such as + 'sess_expire_on_close' when any of the above is not configured. + However, you should never rely on this behavior as it can cause + unexpected results or be changed in the future. Please configure + everything properly. In addition to the values above, the cookie and native drivers apply the following configuration values shared by the :doc:`IncomingRequest ` and @@ -472,9 +472,9 @@ Preference Default Description ==================== =============== =========================================================================== .. note:: The 'cookieHTTPOnly' setting doesn't have an effect on sessions. - Instead the HttpOnly parameter is always enabled, for security - reasons. Additionally, the 'cookiePrefix' setting is completely - ignored. + Instead the HttpOnly parameter is always enabled, for security + reasons. Additionally, the 'cookiePrefix' setting is completely + ignored. Session Drivers *************** @@ -533,9 +533,9 @@ permissions will probably break your application. Instead, you should do something like this, depending on your environment :: - mkdir //Writable/sessions/ - chmod 0700 //Writable/sessions/ - chown www-data //Writable/sessions/ + mkdir //Writable/sessions/ + chmod 0700 //Writable/sessions/ + chown www-data //Writable/sessions/ Bonus Tip --------- @@ -571,43 +571,43 @@ table that we already mentioned and then set it as your For example, if you would like to use 'ci_sessions' as your table name, you would do this:: - public $sessionDriver = 'CodeIgniter\Session\Handlers\DatabaseHandler'; - public $sessionSavePath = 'ci_sessions'; + public $sessionDriver = 'CodeIgniter\Session\Handlers\DatabaseHandler'; + public $sessionSavePath = 'ci_sessions'; And then of course, create the database table ... For MySQL:: - CREATE TABLE IF NOT EXISTS `ci_sessions` ( - `id` varchar(128) NOT null, - `ip_address` varchar(45) NOT null, - `timestamp` timestamp DEFAULT CURRENT_TIMESTAMP NOT null, - `data` blob NOT null, - KEY `ci_sessions_timestamp` (`timestamp`) - ); + CREATE TABLE IF NOT EXISTS `ci_sessions` ( + `id` varchar(128) NOT null, + `ip_address` varchar(45) NOT null, + `timestamp` timestamp DEFAULT CURRENT_TIMESTAMP NOT null, + `data` blob NOT null, + KEY `ci_sessions_timestamp` (`timestamp`) + ); For PostgreSQL:: - CREATE TABLE "ci_sessions" ( - "id" varchar(128) NOT NULL, - "ip_address" inet NOT NULL, - "timestamp" timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, - "data" bytea DEFAULT '' NOT NULL - ); + CREATE TABLE "ci_sessions" ( + "id" varchar(128) NOT NULL, + "ip_address" inet NOT NULL, + "timestamp" timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + "data" bytea DEFAULT '' NOT NULL + ); - CREATE INDEX "ci_sessions_timestamp" ON "ci_sessions" ("timestamp"); + CREATE INDEX "ci_sessions_timestamp" ON "ci_sessions" ("timestamp"); You will also need to add a PRIMARY KEY **depending on your 'sessionMatchIP' setting**. The examples below work both on MySQL and PostgreSQL:: - // When sessionMatchIP = true - ALTER TABLE ci_sessions ADD PRIMARY KEY (id, ip_address); + // When sessionMatchIP = true + ALTER TABLE ci_sessions ADD PRIMARY KEY (id, ip_address); - // When sessionMatchIP = false - ALTER TABLE ci_sessions ADD PRIMARY KEY (id); + // When sessionMatchIP = false + ALTER TABLE ci_sessions ADD PRIMARY KEY (id); - // To drop a previously created primary key (use when changing the setting) - ALTER TABLE ci_sessions DROP PRIMARY KEY; + // To drop a previously created primary key (use when changing the setting) + ALTER TABLE ci_sessions DROP PRIMARY KEY; You can choose the Database group to use by adding a new line to the **app/Config/App.php** file with the name of the group to use:: @@ -624,19 +624,19 @@ This command will take the **sessionSavePath** and **sessionMatchIP** settings i when it generates the code. .. important:: Only MySQL and PostgreSQL databases are officially - supported, due to lack of advisory locking mechanisms on other - platforms. Using sessions without locks can cause all sorts of - problems, especially with heavy usage of AJAX, and we will not - support such cases. Use ``session_write_close()`` after you've - done processing session data if you're having performance - issues. + supported, due to lack of advisory locking mechanisms on other + platforms. Using sessions without locks can cause all sorts of + problems, especially with heavy usage of AJAX, and we will not + support such cases. Use ``session_write_close()`` after you've + done processing session data if you're having performance + issues. RedisHandler Driver =================== .. note:: Since Redis doesn't have a locking mechanism exposed, locks for - this driver are emulated by a separate value that is kept for up - to 300 seconds. + this driver are emulated by a separate value that is kept for up + to 300 seconds. Redis is a storage engine typically used for caching and popular because of its high performance, which is also probably your reason to use the @@ -656,24 +656,24 @@ The format here is a bit different and complicated at the same time. It is best explained by the *phpredis* extension's README file, so we'll simply link you to it: - https://github.com/phpredis/phpredis + https://github.com/phpredis/phpredis .. warning:: CodeIgniter's Session library does NOT use the actual 'redis' - ``session.save_handler``. Take note **only** of the path format in - the link above. + ``session.save_handler``. Take note **only** of the path format in + the link above. For the most common case however, a simple ``host:port`` pair should be sufficient:: - public $sessionDiver = 'CodeIgniter\Session\Handlers\RedisHandler'; - public $sessionSavePath = 'tcp://localhost:6379'; + public $sessionDiver = 'CodeIgniter\Session\Handlers\RedisHandler'; + public $sessionSavePath = 'tcp://localhost:6379'; MemcachedHandler Driver ======================= .. note:: Since Memcached doesn't have a locking mechanism exposed, locks - for this driver are emulated by a separate value that is kept for - up to 300 seconds. + for this driver are emulated by a separate value that is kept for + up to 300 seconds. The 'MemcachedHandler' driver is very similar to the 'RedisHandler' one in all of its properties, except perhaps for availability, because PHP's `Memcached @@ -693,8 +693,8 @@ considered as it may result in loss of sessions. The ``$sessionSavePath`` format is fairly straightforward here, being just a ``host:port`` pair:: - public $sessionDriver = 'CodeIgniter\Session\Handlers\MemcachedHandler'; - public $sessionSavePath = 'localhost:11211'; + public $sessionDriver = 'CodeIgniter\Session\Handlers\MemcachedHandler'; + public $sessionSavePath = 'localhost:11211'; Bonus Tip --------- @@ -706,6 +706,6 @@ to note that we haven't tested if that is reliable. If you want to experiment with this feature (on your own risk), simply separate the multiple server paths with commas:: - // localhost will be given higher priority (5) here, - // compared to 192.0.2.1 with a weight of 1. - public $sessionSavePath = 'localhost:11211:5,192.0.2.1:11211:1'; + // localhost will be given higher priority (5) here, + // compared to 192.0.2.1 with a weight of 1. + public $sessionSavePath = 'localhost:11211:5,192.0.2.1:11211:1'; diff --git a/user_guide_src/source/libraries/user_agent.rst b/user_guide_src/source/libraries/user_agent.rst index f8cf92f72064..327a943f9749 100644 --- a/user_guide_src/source/libraries/user_agent.rst +++ b/user_guide_src/source/libraries/user_agent.rst @@ -20,7 +20,7 @@ The User Agent class is always available directly from the current :doc:`Incomin By default, you will have a request instance in your controller that you can retrieve the User Agent class from:: - $agent = $this->request->getUserAgent(); + $agent = $this->request->getUserAgent(); User Agent Definitions ====================== @@ -37,21 +37,21 @@ whether the user agent browsing your site is a web browser, a mobile device, or a robot. It will also gather the platform information if it is available:: - $agent = $this->request->getUserAgent(); + $agent = $this->request->getUserAgent(); - if ($agent->isBrowser()) { - $currentAgent = $agent->getBrowser().' '.$agent->getVersion(); - } elseif ($agent->isRobot()) { - $currentAgent = $this->agent->robot(); - } elseif ($agent->isMobile()) { - $currentAgent = $agent->getMobile(); - } else { - $currentAgent = 'Unidentified User Agent'; - } + if ($agent->isBrowser()) { + $currentAgent = $agent->getBrowser().' '.$agent->getVersion(); + } elseif ($agent->isRobot()) { + $currentAgent = $this->agent->robot(); + } elseif ($agent->isMobile()) { + $currentAgent = $agent->getMobile(); + } else { + $currentAgent = 'Unidentified User Agent'; + } - echo $currentAgent; + echo $currentAgent; - echo $agent->getPlatform(); // Platform info (Windows, Linux, Mac, etc.) + echo $agent->getPlatform(); // Platform info (Windows, Linux, Mac, etc.) *************** Class Reference @@ -59,120 +59,120 @@ Class Reference .. php:class:: CodeIgniter\\HTTP\\UserAgent - .. php:method:: isBrowser([$key = null]) + .. php:method:: isBrowser([$key = null]) - :param string $key: Optional browser name - :returns: true if the user agent is a (specified) browser, false if not - :rtype: bool + :param string $key: Optional browser name + :returns: true if the user agent is a (specified) browser, false if not + :rtype: bool - Returns true/false (boolean) if the user agent is a known web browser. - :: + Returns true/false (boolean) if the user agent is a known web browser. + :: - if ($agent->isBrowser('Safari')) { - echo 'You are using Safari.'; - } elseif ($agent->isBrowser()) { - echo 'You are using a browser.'; - } + if ($agent->isBrowser('Safari')) { + echo 'You are using Safari.'; + } elseif ($agent->isBrowser()) { + echo 'You are using a browser.'; + } - .. note:: The string "Safari" in this example is an array key in the list of browser definitions. - You can find this list in **app/Config/UserAgents.php** if you want to add new - browsers or change the strings. + .. note:: The string "Safari" in this example is an array key in the list of browser definitions. + You can find this list in **app/Config/UserAgents.php** if you want to add new + browsers or change the strings. - .. php:method:: isMobile([$key = null]) + .. php:method:: isMobile([$key = null]) - :param string $key: Optional mobile device name - :returns: true if the user agent is a (specified) mobile device, false if not - :rtype: bool + :param string $key: Optional mobile device name + :returns: true if the user agent is a (specified) mobile device, false if not + :rtype: bool - Returns true/false (boolean) if the user agent is a known mobile device. - :: + Returns true/false (boolean) if the user agent is a known mobile device. + :: - if ($agent->isMobile('iphone')) { - echo view('iphone/home'); - } elseif ($agent->isMobile()) { - echo view('mobile/home'); - } else { - echo view('web/home'); - } + if ($agent->isMobile('iphone')) { + echo view('iphone/home'); + } elseif ($agent->isMobile()) { + echo view('mobile/home'); + } else { + echo view('web/home'); + } - .. php:method:: isRobot([$key = null]) + .. php:method:: isRobot([$key = null]) - :param string $key: Optional robot name - :returns: true if the user agent is a (specified) robot, false if not - :rtype: bool + :param string $key: Optional robot name + :returns: true if the user agent is a (specified) robot, false if not + :rtype: bool - Returns true/false (boolean) if the user agent is a known robot. + Returns true/false (boolean) if the user agent is a known robot. - .. note:: The user agent library only contains the most common robot definitions. It is not a complete list of bots. - There are hundreds of them so searching for each one would not be very efficient. If you find that some bots - that commonly visit your site are missing from the list you can add them to your - **app/Config/UserAgents.php** file. + .. note:: The user agent library only contains the most common robot definitions. It is not a complete list of bots. + There are hundreds of them so searching for each one would not be very efficient. If you find that some bots + that commonly visit your site are missing from the list you can add them to your + **app/Config/UserAgents.php** file. - .. php:method:: isReferral() + .. php:method:: isReferral() - :returns: true if the user agent is a referral, false if not - :rtype: bool + :returns: true if the user agent is a referral, false if not + :rtype: bool - Returns true/false (boolean) if the user agent was referred from another site. + Returns true/false (boolean) if the user agent was referred from another site. - .. php:method:: getBrowser() + .. php:method:: getBrowser() - :returns: Detected browser or an empty string - :rtype: string + :returns: Detected browser or an empty string + :rtype: string - Returns a string containing the name of the web browser viewing your site. + Returns a string containing the name of the web browser viewing your site. - .. php:method:: getVersion() + .. php:method:: getVersion() - :returns: Detected browser version or an empty string - :rtype: string + :returns: Detected browser version or an empty string + :rtype: string - Returns a string containing the version number of the web browser viewing your site. + Returns a string containing the version number of the web browser viewing your site. - .. php:method:: getMobile() + .. php:method:: getMobile() - :returns: Detected mobile device brand or an empty string - :rtype: string + :returns: Detected mobile device brand or an empty string + :rtype: string - Returns a string containing the name of the mobile device viewing your site. + Returns a string containing the name of the mobile device viewing your site. - .. php:method:: getRobot() + .. php:method:: getRobot() - :returns: Detected robot name or an empty string - :rtype: string + :returns: Detected robot name or an empty string + :rtype: string - Returns a string containing the name of the robot viewing your site. + Returns a string containing the name of the robot viewing your site. - .. php:method:: getPlatform() + .. php:method:: getPlatform() - :returns: Detected operating system or an empty string - :rtype: string + :returns: Detected operating system or an empty string + :rtype: string - Returns a string containing the platform viewing your site (Linux, Windows, OS X, etc.). + Returns a string containing the platform viewing your site (Linux, Windows, OS X, etc.). - .. php:method:: getReferrer() + .. php:method:: getReferrer() - :returns: Detected referrer or an empty string - :rtype: string + :returns: Detected referrer or an empty string + :rtype: string - The referrer, if the user agent was referred from another site. Typically you'll test for this as follows:: + The referrer, if the user agent was referred from another site. Typically you'll test for this as follows:: - if ($agent->isReferral()) { - echo $agent->referrer(); - } + if ($agent->isReferral()) { + echo $agent->referrer(); + } - .. php:method:: getAgentString() + .. php:method:: getAgentString() - :returns: Full user agent string or an empty string - :rtype: string + :returns: Full user agent string or an empty string + :rtype: string - Returns a string containing the full user agent string. Typically it will be something like this:: + Returns a string containing the full user agent string. Typically it will be something like this:: - Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.4) Gecko/20060613 Camino/1.0.2 + Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.4) Gecko/20060613 Camino/1.0.2 - .. php:method:: parse($string) + .. php:method:: parse($string) - :param string $string: A custom user-agent string - :rtype: void + :param string $string: A custom user-agent string + :rtype: void - Parses a custom user-agent string, different from the one reported by the current visitor. + Parses a custom user-agent string, different from the one reported by the current visitor. diff --git a/user_guide_src/source/outgoing/table.rst b/user_guide_src/source/outgoing/table.rst index 334658d08380..0c8810fdb277 100644 --- a/user_guide_src/source/outgoing/table.rst +++ b/user_guide_src/source/outgoing/table.rst @@ -18,7 +18,7 @@ Initializing the Class The Table class is not provided as a service, and should be instantiated "normally", for instance:: - $table = new \CodeIgniter\View\Table(); + $table = new \CodeIgniter\View\Table(); Examples ======== @@ -30,16 +30,16 @@ method described in the function reference below). :: - $table = new \CodeIgniter\View\Table(); + $table = new \CodeIgniter\View\Table(); - $data = [ - ['Name', 'Color', 'Size'], - ['Fred', 'Blue', 'Small'], - ['Mary', 'Red', 'Large'], - ['John', 'Green', 'Medium'], - ]; + $data = [ + ['Name', 'Color', 'Size'], + ['Fred', 'Blue', 'Small'], + ['Mary', 'Red', 'Large'], + ['John', 'Green', 'Medium'], + ]; - echo $table->generate($data); + echo $table->generate($data); Here is an example of a table created from a database query result. The table class will automatically generate the headings based on the table @@ -48,37 +48,37 @@ method described in the class reference below). :: - $table = new \CodeIgniter\View\Table(); + $table = new \CodeIgniter\View\Table(); - $query = $db->query('SELECT * FROM my_table'); + $query = $db->query('SELECT * FROM my_table'); - echo $table->generate($query); + echo $table->generate($query); Here is an example showing how you might create a table using discrete parameters:: - $table = new \CodeIgniter\View\Table(); + $table = new \CodeIgniter\View\Table(); - $table->setHeading('Name', 'Color', 'Size'); + $table->setHeading('Name', 'Color', 'Size'); - $table->addRow('Fred', 'Blue', 'Small'); - $table->addRow('Mary', 'Red', 'Large'); - $table->addRow('John', 'Green', 'Medium'); + $table->addRow('Fred', 'Blue', 'Small'); + $table->addRow('Mary', 'Red', 'Large'); + $table->addRow('John', 'Green', 'Medium'); - echo $table->generate(); + echo $table->generate(); Here is the same example, except instead of individual parameters, arrays are used:: - $table = new \CodeIgniter\View\Table(); + $table = new \CodeIgniter\View\Table(); - $table->setHeading(array('Name', 'Color', 'Size')); + $table->setHeading(array('Name', 'Color', 'Size')); - $table->addRow(['Fred', 'Blue', 'Small']); - $table->addRow(['Mary', 'Red', 'Large']); - $table->addRow(['John', 'Green', 'Medium']); + $table->addRow(['Fred', 'Blue', 'Small']); + $table->addRow(['Mary', 'Red', 'Large']); + $table->addRow(['John', 'Green', 'Medium']); - echo $table->generate(); + echo $table->generate(); Changing the Look of Your Table =============================== @@ -86,65 +86,65 @@ Changing the Look of Your Table The Table Class permits you to set a table template with which you can specify the design of your layout. Here is the template prototype:: - $template = [ - 'table_open' => '', + $template = [ + 'table_open' => '
    ', - 'thead_open' => '', - 'thead_close' => '', + 'thead_open' => '', + 'thead_close' => '', - 'heading_row_start' => '', - 'heading_row_end' => '', - 'heading_cell_start' => '', + 'heading_row_start' => '', + 'heading_row_end' => '', + 'heading_cell_start' => '', - 'tfoot_open' => '', - 'tfoot_close' => '', + 'tfoot_open' => '', + 'tfoot_close' => '', - 'footing_row_start' => '', - 'footing_row_end' => '', - 'footing_cell_start' => '', + 'footing_row_start' => '', + 'footing_row_end' => '', + 'footing_cell_start' => '', - 'tbody_open' => '', - 'tbody_close' => '', + 'tbody_open' => '', + 'tbody_close' => '', - 'row_start' => '', - 'row_end' => '', - 'cell_start' => '', + 'row_start' => '', + 'row_end' => '', + 'cell_start' => '', - 'row_alt_start' => '', - 'row_alt_end' => '', - 'cell_alt_start' => '', + 'row_alt_start' => '', + 'row_alt_end' => '', + 'cell_alt_start' => '', - 'table_close' => '
    ', - 'heading_cell_end' => '
    ', + 'heading_cell_end' => '
    ', - 'footing_cell_end' => '
    ', + 'footing_cell_end' => '
    ', - 'cell_end' => '
    ', + 'cell_end' => '
    ', - 'cell_alt_end' => '
    ', + 'cell_alt_end' => '
    ' - ]; + 'table_close' => '' + ]; - $table->setTemplate($template); + $table->setTemplate($template); .. note:: You'll notice there are two sets of "row" blocks in the - template. These permit you to create alternating row colors or design - elements that alternate with each iteration of the row data. + template. These permit you to create alternating row colors or design + elements that alternate with each iteration of the row data. You are NOT required to submit a complete template. If you only need to change parts of the layout you can simply submit those elements. In this example, only the table opening tag is being changed:: - $template = [ - 'table_open' => '' - ]; + $template = [ + 'table_open' => '
    ' + ]; - $table->setTemplate($template); + $table->setTemplate($template); You can also set defaults for these by passing an array of template settings to the Table constructor.:: - $customSettings = [ - 'table_open' => '
    ' - ]; + $customSettings = [ + 'table_open' => '
    ' + ]; - $table = new \CodeIgniter\View\Table($customSettings); + $table = new \CodeIgniter\View\Table($customSettings); *************** @@ -153,171 +153,171 @@ Class Reference .. php:class:: Table - .. attribute:: $function = null + .. attribute:: $function = null - Allows you to specify a native PHP function or a valid function array object to be applied to all cell data. - :: + Allows you to specify a native PHP function or a valid function array object to be applied to all cell data. + :: - $table = new \CodeIgniter\View\Table(); + $table = new \CodeIgniter\View\Table(); - $table->setHeading('Name', 'Color', 'Size'); - $table->addRow('Fred', 'Blue', 'Small'); + $table->setHeading('Name', 'Color', 'Size'); + $table->addRow('Fred', 'Blue', 'Small'); - $table->function = 'htmlspecialchars'; - echo $table->generate(); + $table->function = 'htmlspecialchars'; + echo $table->generate(); - In the above example, all cell data would be run through PHP's :php:func:`htmlspecialchars()` function, resulting in:: + In the above example, all cell data would be run through PHP's :php:func:`htmlspecialchars()` function, resulting in:: - + - .. php:method:: generate([$tableData = null]) + .. php:method:: generate([$tableData = null]) - :param mixed $tableData: Data to populate the table rows with - :returns: HTML table - :rtype: string + :param mixed $tableData: Data to populate the table rows with + :returns: HTML table + :rtype: string - Returns a string containing the generated table. Accepts an optional parameter which can be an array or a database result object. + Returns a string containing the generated table. Accepts an optional parameter which can be an array or a database result object. - .. php:method:: setCaption($caption) + .. php:method:: setCaption($caption) - :param string $caption: Table caption - :returns: Table instance (method chaining) - :rtype: Table + :param string $caption: Table caption + :returns: Table instance (method chaining) + :rtype: Table - Permits you to add a caption to the table. - :: + Permits you to add a caption to the table. + :: - $table->setCaption('Colors'); + $table->setCaption('Colors'); - .. php:method:: setHeading([$args = [] [, ...]]) + .. php:method:: setHeading([$args = [] [, ...]]) - :param mixed $args: An array or multiple strings containing the table column titles - :returns: Table instance (method chaining) - :rtype: Table + :param mixed $args: An array or multiple strings containing the table column titles + :returns: Table instance (method chaining) + :rtype: Table - Permits you to set the table heading. You can submit an array or discrete params:: + Permits you to set the table heading. You can submit an array or discrete params:: - $table->setHeading('Name', 'Color', 'Size'); // or + $table->setHeading('Name', 'Color', 'Size'); // or - $table->setHeading(['Name', 'Color', 'Size']); + $table->setHeading(['Name', 'Color', 'Size']); - .. php:method:: setFooting([$args = [] [, ...]]) + .. php:method:: setFooting([$args = [] [, ...]]) - :param mixed $args: An array or multiple strings containing the table footing values - :returns: Table instance (method chaining) - :rtype: Table + :param mixed $args: An array or multiple strings containing the table footing values + :returns: Table instance (method chaining) + :rtype: Table - Permits you to set the table footing. You can submit an array or discrete params:: + Permits you to set the table footing. You can submit an array or discrete params:: - $table->setFooting('Subtotal', $subtotal, $notes); // or + $table->setFooting('Subtotal', $subtotal, $notes); // or - $table->setFooting(['Subtotal', $subtotal, $notes]); + $table->setFooting(['Subtotal', $subtotal, $notes]); - .. php:method:: addRow([$args = [] [, ...]]) + .. php:method:: addRow([$args = [] [, ...]]) - :param mixed $args: An array or multiple strings containing the row values - :returns: Table instance (method chaining) - :rtype: Table + :param mixed $args: An array or multiple strings containing the row values + :returns: Table instance (method chaining) + :rtype: Table - Permits you to add a row to your table. You can submit an array or discrete params:: + Permits you to add a row to your table. You can submit an array or discrete params:: - $table->addRow('Blue', 'Red', 'Green'); // or + $table->addRow('Blue', 'Red', 'Green'); // or - $table->addRow(['Blue', 'Red', 'Green']); + $table->addRow(['Blue', 'Red', 'Green']); - If you would like to set an individual cell's tag attributes, you can use an associative array for that cell. - The associative key **data** defines the cell's data. Any other key => val pairs are added as key='val' attributes to the tag:: + If you would like to set an individual cell's tag attributes, you can use an associative array for that cell. + The associative key **data** defines the cell's data. Any other key => val pairs are added as key='val' attributes to the tag:: - $cell = ['data' => 'Blue', 'class' => 'highlight', 'colspan' => 2]; - $table->addRow($cell, 'Red', 'Green'); + $cell = ['data' => 'Blue', 'class' => 'highlight', 'colspan' => 2]; + $table->addRow($cell, 'Red', 'Green'); - // generates - // + // generates + // - .. php:method:: makeColumns([$array = [] [, $columnLimit = 0]]) + .. php:method:: makeColumns([$array = [] [, $columnLimit = 0]]) - :param array $array: An array containing multiple rows' data - :param int $columnLimit: Count of columns in the table - :returns: An array of HTML table columns - :rtype: array + :param array $array: An array containing multiple rows' data + :param int $columnLimit: Count of columns in the table + :returns: An array of HTML table columns + :rtype: array - This method takes a one-dimensional array as input and creates a multi-dimensional array with a depth equal to the number of columns desired. - This allows a single array with many elements to be displayed in a table that has a fixed column count. Consider this example:: + This method takes a one-dimensional array as input and creates a multi-dimensional array with a depth equal to the number of columns desired. + This allows a single array with many elements to be displayed in a table that has a fixed column count. Consider this example:: - $list = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve']; + $list = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve']; - $newList = $table->makeColumns($list, 3); + $newList = $table->makeColumns($list, 3); - $table->generate($newList); + $table->generate($newList); - // Generates a table with this prototype + // Generates a table with this prototype -
    Fred<strong>Blue</strong>SmallFred<strong>Blue</strong>SmallBlueRedGreenBlueRedGreen
    - - - - - - - - -
    onetwothree
    fourfivesix
    seveneightnine
    teneleventwelve
    + + + + + + + + + +
    onetwothree
    fourfivesix
    seveneightnine
    teneleventwelve
    - .. php:method:: setTemplate($template) + .. php:method:: setTemplate($template) - :param array $template: An associative array containing template values - :returns: true on success, false on failure - :rtype: bool + :param array $template: An associative array containing template values + :returns: true on success, false on failure + :rtype: bool - Permits you to set your template. You can submit a full or partial template. - :: + Permits you to set your template. You can submit a full or partial template. + :: - $template = [ - 'table_open' => '' - ]; + $template = [ + 'table_open' => '
    ' + ]; - $table->setTemplate($template); + $table->setTemplate($template); - .. php:method:: setEmpty($value) + .. php:method:: setEmpty($value) - :param mixed $value: Value to put in empty cells - :returns: Table instance (method chaining) - :rtype: Table + :param mixed $value: Value to put in empty cells + :returns: Table instance (method chaining) + :rtype: Table - Lets you set a default value for use in any table cells that are empty. - You might, for example, set a non-breaking space:: + Lets you set a default value for use in any table cells that are empty. + You might, for example, set a non-breaking space:: - $table->setEmpty(" "); + $table->setEmpty(" "); - .. php:method:: clear() + .. php:method:: clear() - :returns: Table instance (method chaining) - :rtype: Table + :returns: Table instance (method chaining) + :rtype: Table - Lets you clear the table heading, row data and caption. If - you need to show multiple tables with different data you - should to call this method after each table has been - generated to clear the previous table information. + Lets you clear the table heading, row data and caption. If + you need to show multiple tables with different data you + should to call this method after each table has been + generated to clear the previous table information. - Example :: + Example :: - $table = new \CodeIgniter\View\Table(); + $table = new \CodeIgniter\View\Table(); - $table->setCaption('Preferences') - ->setHeading('Name', 'Color', 'Size') - ->addRow('Fred', 'Blue', 'Small') - ->addRow('Mary', 'Red', 'Large') - ->addRow('John', 'Green', 'Medium'); + $table->setCaption('Preferences') + ->setHeading('Name', 'Color', 'Size') + ->addRow('Fred', 'Blue', 'Small') + ->addRow('Mary', 'Red', 'Large') + ->addRow('John', 'Green', 'Medium'); - echo $table->generate(); + echo $table->generate(); - $table->clear(); + $table->clear(); - $table->setCaption('Shipping') - ->setHeading('Name', 'Day', 'Delivery') - ->addRow('Fred', 'Wednesday', 'Express') - ->addRow('Mary', 'Monday', 'Air') - ->addRow('John', 'Saturday', 'Overnight'); + $table->setCaption('Shipping') + ->setHeading('Name', 'Day', 'Delivery') + ->addRow('Fred', 'Wednesday', 'Express') + ->addRow('Mary', 'Monday', 'Air') + ->addRow('John', 'Saturday', 'Overnight'); - echo $table->generate(); + echo $table->generate(); diff --git a/user_guide_src/source/outgoing/views.rst b/user_guide_src/source/outgoing/views.rst index 0ea2001fe724..19121c98b86a 100644 --- a/user_guide_src/source/outgoing/views.rst +++ b/user_guide_src/source/outgoing/views.rst @@ -21,14 +21,14 @@ Creating a View Using your text editor, create a file called ``BlogView.php`` and put this in it:: - + My Blog

    Welcome to my Blog!

    - + Then save the file in your **app/Views** directory. @@ -37,7 +37,7 @@ Displaying a View To load and display a particular view file you will use the following function:: - echo view('name'); + echo view('name'); Where *name* is the name of your view file. @@ -45,21 +45,21 @@ Where *name* is the name of your view file. Now, open the controller file you made earlier called ``Blog.php``, and replace the echo statement with the view function:: - 'Your title', - ]; + class Page extends \CodeIgniter\Controller + { + public function index() + { + $data = [ + 'page_title' => 'Your title', + ]; - echo view('header'); - echo view('menu'); - echo view('content', $data); - echo view('footer'); - } - } + echo view('header'); + echo view('menu'); + echo view('content', $data); + echo view('footer'); + } + } In the example above, we are using "dynamically added data", which you will see below. @@ -98,7 +98,7 @@ Storing Views within Sub-directories Your view files can also be stored within sub-directories if you prefer that type of organization. When doing so you will need to include the directory name loading the view. Example:: - echo view('directory_name/file_name'); + echo view('directory_name/file_name'); Namespaced Views ================ @@ -134,41 +134,41 @@ Adding Dynamic Data to the View Data is passed from the controller to the view by way of an array in the second parameter of the view function. Here's an example:: - $data = [ - 'title' => 'My title', - 'heading' => 'My Heading', - 'message' => 'My Message', - ]; + $data = [ + 'title' => 'My title', + 'heading' => 'My Heading', + 'message' => 'My Message', + ]; - echo view('blogview', $data); + echo view('blogview', $data); Let's try it with your controller file. Open it and add this code:: - + <?= $title ?>

    - + Then load the page at the URL you've been using and you should see the variables replaced. @@ -178,13 +178,13 @@ other views, potentially causing issues. If you would prefer the data to persist into the `$option` array in the third parameter. :: - $data = [ - 'title' => 'My title', - 'heading' => 'My Heading', - 'message' => 'My Message', - ]; + $data = [ + 'title' => 'My title', + 'heading' => 'My Heading', + 'message' => 'My Message', + ]; - echo view('blogview', $data, ['saveData' => true]); + echo view('blogview', $data, ['saveData' => true]); Additionally, if you would like the default functionality of the view function to be that it does save the data between calls, you can set ``$saveData`` to **true** in **app/Config/Views.php**. @@ -198,42 +198,42 @@ typically be in the form of a multi-dimensional array. Here’s a simple example. Add this to your controller:: - ['Clean House', 'Call Mom', 'Run Errands'], - 'title' => 'My Real Title', - 'heading' => 'My Real Heading', - ]; + class Blog extends \CodeIgniter\Controller + { + public function index() + { + $data = [ + 'todo_list' => ['Clean House', 'Call Mom', 'Run Errands'], + 'title' => 'My Real Title', + 'heading' => 'My Real Heading', + ]; - echo view('blogview', $data); - } - } + echo view('blogview', $data); + } + } Now open your view file and create a loop:: - - - <?= $title ?> - - -

    + + + <?= $title ?> + + +

    -

    My Todo List

    +

    My Todo List

    -
      - +
        + -
      • +
      • - -
      + +
    - - + + From ec5a92540fe674afbca9253aa2056fd479456b14 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 21 Sep 2021 20:10:58 +0900 Subject: [PATCH 136/490] docs: replace tabs with spaces --- user_guide_src/ghpages.rst | 10 +- .../source/database/query_builder.rst | 280 +++++++++--------- user_guide_src/source/helpers/form_helper.rst | 220 +++++++------- 3 files changed, 255 insertions(+), 255 deletions(-) diff --git a/user_guide_src/ghpages.rst b/user_guide_src/ghpages.rst index 1d7719c1193c..4699d38f978d 100644 --- a/user_guide_src/ghpages.rst +++ b/user_guide_src/ghpages.rst @@ -26,19 +26,19 @@ Re-generating the User Guide In the ``user_guide_src`` folder, you generate a conventional user guide, for testing, using the command:: - make html + make html An additional target has been configured, which will generate the same HTML but inside the ``html`` folder of the second repo clone:: - make ghpages + make ghpages After making this target, update the online user guide by switching to the ``CodeIgniter4-guide/html`` folder, and then:: - git add . - git commit -S -m "Suitable comment" - git push origin gh-pages + git add . + git commit -S -m "Suitable comment" + git push origin gh-pages Process ======= diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 45380416fdac..26fcba73e4ee 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -627,7 +627,7 @@ searches. $builder->havingLike('title', 'match', 'before'); // Produces: HAVING `title` LIKE '%match' ESCAPE '!' $builder->havingLike('title', 'match', 'after'); // Produces: HAVING `title` LIKE 'match%' ESCAPE '!' - $builder->havingLike('title', 'match', 'both'); // Produces: HAVING `title` LIKE '%match%' ESCAPE '!' + $builder->havingLike('title', 'match', 'both'); // Produces: HAVING `title` LIKE '%match%' ESCAPE '!' #. **Associative array method:** @@ -1046,9 +1046,9 @@ is an example using an array:: $builder->update($data); // Produces: // - // UPDATE mytable - // SET title = '{$title}', name = '{$name}', date = '{$date}' - // WHERE id = $id + // UPDATE mytable + // SET title = '{$title}', name = '{$name}', date = '{$date}' + // WHERE id = $id Or you can supply an object:: @@ -1238,8 +1238,8 @@ Class Reference .. php:method:: db() - :returns: The database connection in use - :rtype: ``ConnectionInterface`` + :returns: The database connection in use + :rtype: ``ConnectionInterface`` Returns the current database connection from ``$db``. Useful for accessing ``ConnectionInterface`` methods that are not directly @@ -1247,8 +1247,8 @@ Class Reference .. php:method:: resetQuery() - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Resets the current Query Builder state. Useful when you want to build a query that can be canceled under certain conditions. @@ -1256,8 +1256,8 @@ Class Reference .. php:method:: countAllResults([$reset = true]) :param bool $reset: Whether to reset values for SELECTs - :returns: Number of rows in the query result - :rtype: int + :returns: Number of rows in the query result + :rtype: int Generates a platform-specific query string that counts all records returned by an Query Builder query. @@ -1265,8 +1265,8 @@ Class Reference .. php:method:: countAll([$reset = true]) :param bool $reset: Whether to reset values for SELECTs - :returns: Number of rows in the query result - :rtype: int + :returns: Number of rows in the query result + :rtype: int Generates a platform-specific query string that counts all records in the particular table. @@ -1277,7 +1277,7 @@ Class Reference :param int $offset: The OFFSET clause :param bool $reset: Do we want to clear query builder values? :returns: ``\CodeIgniter\Database\ResultInterface`` instance (method chaining) - :rtype: ``\CodeIgniter\Database\ResultInterface`` + :rtype: ``\CodeIgniter\Database\ResultInterface`` Compiles and runs ``SELECT`` statement based on the already called Query Builder methods. @@ -1288,8 +1288,8 @@ Class Reference :param int $limit: The LIMIT clause :param int $offset: The OFFSET clause :param bool $reset: Do we want to clear query builder values? - :returns: ``\CodeIgniter\Database\ResultInterface`` instance (method chaining) - :rtype: ``\CodeIgniter\Database\ResultInterface`` + :returns: ``\CodeIgniter\Database\ResultInterface`` instance (method chaining) + :rtype: ``\CodeIgniter\Database\ResultInterface`` Same as ``get()``, but also allows the WHERE to be added directly. @@ -1297,8 +1297,8 @@ Class Reference :param string $select: The SELECT portion of a query :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``SELECT`` clause to a query. @@ -1306,8 +1306,8 @@ Class Reference :param string $select: Field to compute the average of :param string $alias: Alias for the resulting value name - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``SELECT AVG(field)`` clause to a query. @@ -1315,8 +1315,8 @@ Class Reference :param string $select: Field to compute the maximum of :param string $alias: Alias for the resulting value name - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``SELECT MAX(field)`` clause to a query. @@ -1324,8 +1324,8 @@ Class Reference :param string $select: Field to compute the minimum of :param string $alias: Alias for the resulting value name - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``SELECT MIN(field)`` clause to a query. @@ -1333,8 +1333,8 @@ Class Reference :param string $select: Field to compute the sum of :param string $alias: Alias for the resulting value name - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``SELECT SUM(field)`` clause to a query. @@ -1342,16 +1342,16 @@ Class Reference :param string $select: Field to compute the average of :param string $alias: Alias for the resulting value name - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``SELECT COUNT(field)`` clause to a query. .. php:method:: distinct([$val = true]) :param bool $val: Desired value of the "distinct" flag - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Sets a flag which tells the query builder to add a ``DISTINCT`` clause to the ``SELECT`` portion of the query. @@ -1359,9 +1359,9 @@ Class Reference .. php:method:: from($from[, $overwrite = false]) :param mixed $from: Table name(s); string or array - :param bool $overwrite: Should we remove the first table existing? - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :param bool $overwrite: Should we remove the first table existing? + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Specifies the ``FROM`` clause of a query. @@ -1370,9 +1370,9 @@ Class Reference :param string $table: Table name to join :param string $cond: The JOIN ON condition :param string $type: The JOIN type - :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :param bool $escape: Whether to escape values and identifiers + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``JOIN`` clause to a query. @@ -1380,9 +1380,9 @@ Class Reference :param mixed $key: Name of field to compare, or associative array :param mixed $value: If a single key, compared to this value - :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :param bool $escape: Whether to escape values and identifiers + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Generates the ``WHERE`` portion of the query. Separates multiple calls with ``AND``. @@ -1391,8 +1391,8 @@ Class Reference :param mixed $key: Name of field to compare, or associative array :param mixed $value: If a single key, compared to this value :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Generates the ``WHERE`` portion of the query. Separates multiple calls with ``OR``. @@ -1401,8 +1401,8 @@ Class Reference :param string $key: The field to search :param array|Closure $values: Array of target values, or anonymous function for subquery :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Generates a ``WHERE`` field ``IN('item', 'item')`` SQL query, joined with ``OR`` if appropriate. @@ -1411,8 +1411,8 @@ Class Reference :param string $key: The field to search :param array|Closure $values: Array of target values, or anonymous function for subquery :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Generates a ``WHERE`` field ``NOT IN('item', 'item')`` SQL query, joined with ``OR`` if appropriate. @@ -1421,8 +1421,8 @@ Class Reference :param string $key: Name of field to examine :param array|Closure $values: Array of target values, or anonymous function for subquery :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Generates a ``WHERE`` field ``IN('item', 'item')`` SQL query, joined with ``AND`` if appropriate. @@ -1430,44 +1430,44 @@ Class Reference :param string $key: Name of field to examine :param array|Closure $values: Array of target values, or anonymous function for subquery - :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :param bool $escape: Whether to escape values and identifiers + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Generates a ``WHERE`` field ``NOT IN('item', 'item')`` SQL query, joined with ``AND`` if appropriate. .. php:method:: groupStart() - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Starts a group expression, using ``AND`` for the conditions inside it. .. php:method:: orGroupStart() - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Starts a group expression, using ``OR`` for the conditions inside it. .. php:method:: notGroupStart() - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Starts a group expression, using ``AND NOT`` for the conditions inside it. .. php:method:: orNotGroupStart() - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Starts a group expression, using ``OR NOT`` for the conditions inside it. .. php:method:: groupEnd() - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Ends a group expression. @@ -1476,10 +1476,10 @@ Class Reference :param string $field: Field name :param string $match: Text portion to match :param string $side: Which side of the expression to put the '%' wildcard on - :param bool $escape: Whether to escape values and identifiers + :param bool $escape: Whether to escape values and identifiers :param bool $insensitiveSearch: Whether to force a case-insensitive search - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``LIKE`` clause to a query, separating multiple calls with ``AND``. @@ -1488,10 +1488,10 @@ Class Reference :param string $field: Field name :param string $match: Text portion to match :param string $side: Which side of the expression to put the '%' wildcard on - :param bool $escape: Whether to escape values and identifiers + :param bool $escape: Whether to escape values and identifiers :param bool $insensitiveSearch: Whether to force a case-insensitive search - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``LIKE`` clause to a query, separating multiple class with ``OR``. @@ -1500,10 +1500,10 @@ Class Reference :param string $field: Field name :param string $match: Text portion to match :param string $side: Which side of the expression to put the '%' wildcard on - :param bool $escape: Whether to escape values and identifiers + :param bool $escape: Whether to escape values and identifiers :param bool $insensitiveSearch: Whether to force a case-insensitive search - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``NOT LIKE`` clause to a query, separating multiple calls with ``AND``. @@ -1512,10 +1512,10 @@ Class Reference :param string $field: Field name :param string $match: Text portion to match :param string $side: Which side of the expression to put the '%' wildcard on - :param bool $escape: Whether to escape values and identifiers + :param bool $escape: Whether to escape values and identifiers :param bool $insensitiveSearch: Whether to force a case-insensitive search - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``NOT LIKE`` clause to a query, separating multiple calls with ``OR``. @@ -1524,8 +1524,8 @@ Class Reference :param mixed $key: Identifier (string) or associative array of field/value pairs :param string $value: Value sought if $key is an identifier :param string $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``HAVING`` clause to a query, separating multiple calls with ``AND``. @@ -1534,8 +1534,8 @@ Class Reference :param mixed $key: Identifier (string) or associative array of field/value pairs :param string $value: Value sought if $key is an identifier :param string $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``HAVING`` clause to a query, separating multiple calls with ``OR``. @@ -1543,9 +1543,9 @@ Class Reference :param string $key: The field to search :param array|Closure $values: Array of target values, or anonymous function for subquery - :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :param bool $escape: Whether to escape values and identifiers + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Generates a ``HAVING`` field IN('item', 'item') SQL query, joined with ``OR`` if appropriate. @@ -1553,9 +1553,9 @@ Class Reference :param string $key: The field to search :param array|Closure $values: Array of target values, or anonymous function for subquery - :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :param bool $escape: Whether to escape values and identifiers + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Generates a ``HAVING`` field ``NOT IN('item', 'item')`` SQL query, joined with ``OR`` if appropriate. @@ -1564,8 +1564,8 @@ Class Reference :param string $key: Name of field to examine :param array|Closure $values: Array of target values, or anonymous function for subquery :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Generates a ``HAVING`` field ``IN('item', 'item')`` SQL query, joined with ``AND`` if appropriate. @@ -1575,8 +1575,8 @@ Class Reference :param array|Closure $values: Array of target values, or anonymous function for subquery :param bool $escape: Whether to escape values and identifiers :param bool $insensitiveSearch: Whether to force a case-insensitive search - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Generates a ``HAVING`` field ``NOT IN('item', 'item')`` SQL query, joined with ``AND`` if appropriate. @@ -1585,10 +1585,10 @@ Class Reference :param string $field: Field name :param string $match: Text portion to match :param string $side: Which side of the expression to put the '%' wildcard on - :param bool $escape: Whether to escape values and identifiers + :param bool $escape: Whether to escape values and identifiers :param bool $insensitiveSearch: Whether to force a case-insensitive search - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``LIKE`` clause to a ``HAVING`` part of the query, separating multiple calls with ``AND``. @@ -1597,10 +1597,10 @@ Class Reference :param string $field: Field name :param string $match: Text portion to match :param string $side: Which side of the expression to put the '%' wildcard on - :param bool $escape: Whether to escape values and identifiers + :param bool $escape: Whether to escape values and identifiers :param bool $insensitiveSearch: Whether to force a case-insensitive search :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :rtype: ``BaseBuilder`` Adds a ``LIKE`` clause to a ``HAVING`` part of the query, separating multiple class with ``OR``. @@ -1609,10 +1609,10 @@ Class Reference :param string $field: Field name :param string $match: Text portion to match :param string $side: Which side of the expression to put the '%' wildcard on - :param bool $escape: Whether to escape values and identifiers + :param bool $escape: Whether to escape values and identifiers :param bool $insensitiveSearch: Whether to force a case-insensitive search - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``NOT LIKE`` clause to a ``HAVING`` part of the query, separating multiple calls with ``AND``. @@ -1621,52 +1621,52 @@ Class Reference :param string $field: Field name :param string $match: Text portion to match :param string $side: Which side of the expression to put the '%' wildcard on - :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :param bool $escape: Whether to escape values and identifiers + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``NOT LIKE`` clause to a ``HAVING`` part of the query, separating multiple calls with ``OR``. .. php:method:: havingGroupStart() - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Starts a group expression for ``HAVING`` clause, using ``AND`` for the conditions inside it. .. php:method:: orHavingGroupStart() - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Starts a group expression for ``HAVING`` clause, using ``OR`` for the conditions inside it. .. php:method:: notHavingGroupStart() - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Starts a group expression for ``HAVING`` clause, using ``AND NOT`` for the conditions inside it. .. php:method:: orNotHavingGroupStart() - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Starts a group expression for ``HAVING`` clause, using ``OR NOT`` for the conditions inside it. .. php:method:: havingGroupEnd() - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Ends a group expression for ``HAVING`` clause. .. php:method:: groupBy($by[, $escape = null]) :param mixed $by: Field(s) to group by; string or array - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds a ``GROUP BY`` clause to a query. @@ -1674,9 +1674,9 @@ Class Reference :param string $orderby: Field to order by :param string $direction: The order requested - ASC, DESC or random - :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :param bool $escape: Whether to escape values and identifiers + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds an ``ORDER BY`` clause to a query. @@ -1684,16 +1684,16 @@ Class Reference :param int $value: Number of rows to limit the results to :param int $offset: Number of rows to skip - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds ``LIMIT`` and ``OFFSET`` clauses to a query. .. php:method:: offset($offset) :param int $offset: Number of rows to skip - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds an ``OFFSET`` clause to a query. @@ -1701,9 +1701,9 @@ Class Reference :param mixed $key: Field name, or an array of field/value pairs :param mixed $value: Field value, if $key is a single field - :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :param bool $escape: Whether to escape values and identifiers + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds field/value pairs to be passed later to ``insert()``, ``update()`` or ``replace()``. @@ -1711,8 +1711,8 @@ Class Reference :param array $set: An associative array of field/value pairs :param bool $escape: Whether to escape values and identifiers - :returns: ``true`` on success, ``false`` on failure - :rtype: bool + :returns: ``true`` on success, ``false`` on failure + :rtype: bool Compiles and executes an ``INSERT`` statement. @@ -1722,7 +1722,7 @@ Class Reference :param bool $escape: Whether to escape values and identifiers :param int $batch_size: Count of rows to insert at once :returns: Number of rows inserted or ``false`` on failure - :rtype: int|false + :rtype: int|false Compiles and executes batch ``INSERT`` statements. @@ -1735,8 +1735,8 @@ Class Reference :param mixed $key: Field name or an array of field/value pairs :param string $value: Field value, if $key is a single field :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds field/value pairs to be inserted in a table later via ``insertBatch()``. @@ -1745,8 +1745,8 @@ Class Reference :param array $set: An associative array of field/value pairs :param string $where: The WHERE clause :param int $limit: The LIMIT clause - :returns: ``true`` on success, ``false`` on failure - :rtype: bool + :returns: ``true`` on success, ``false`` on failure + :rtype: bool Compiles and executes an ``UPDATE`` statement. @@ -1755,8 +1755,8 @@ Class Reference :param array $set: Field name, or an associative array of field/value pairs :param string $value: Field value, if $set is a single field :param int $batch_size: Count of conditions to group in a single query - :returns: Number of rows updated or ``false`` on failure - :rtype: int|false + :returns: Number of rows updated or ``false`` on failure + :rtype: int|false Compiles and executes batch ``UPDATE`` statements. @@ -1768,9 +1768,9 @@ Class Reference :param mixed $key: Field name or an array of field/value pairs :param string $value: Field value, if $key is a single field - :param bool $escape: Whether to escape values and identifiers - :returns: ``BaseBuilder`` instance (method chaining) - :rtype: ``BaseBuilder`` + :param bool $escape: Whether to escape values and identifiers + :returns: ``BaseBuilder`` instance (method chaining) + :rtype: ``BaseBuilder`` Adds field/value pairs to be updated in a table later via ``updateBatch()``. @@ -1778,7 +1778,7 @@ Class Reference :param array $set: An associative array of field/value pairs :returns: ``true`` on success, ``false`` on failure - :rtype: bool + :rtype: bool Compiles and executes a ``REPLACE`` statement. @@ -1787,8 +1787,8 @@ Class Reference :param string $where: The WHERE clause :param int $limit: The LIMIT clause :param bool $reset_data: true to reset the query "write" clause - :returns: ``BaseBuilder`` instance (method chaining) or ``false`` on failure - :rtype: ``BaseBuilder|false`` + :returns: ``BaseBuilder`` instance (method chaining) or ``false`` on failure + :rtype: ``BaseBuilder|false`` Compiles and executes a ``DELETE`` query. @@ -1812,8 +1812,8 @@ Class Reference .. php:method:: truncate() - :returns: ``true`` on success, ``false`` on failure, string on test mode - :rtype: bool|string + :returns: ``true`` on success, ``false`` on failure, string on test mode + :rtype: bool|string Executes a ``TRUNCATE`` statement on a table. @@ -1823,7 +1823,7 @@ Class Reference .. php:method:: emptyTable() :returns: ``true`` on success, ``false`` on failure - :rtype: bool + :rtype: bool Deletes all records from a table via a ``DELETE`` statement. @@ -1831,7 +1831,7 @@ Class Reference :param bool $reset: Whether to reset the current QB values or not :returns: The compiled SQL statement as a string - :rtype: string + :rtype: string Compiles a ``SELECT`` statement and returns it as a string. @@ -1839,7 +1839,7 @@ Class Reference :param bool $reset: Whether to reset the current QB values or not :returns: The compiled SQL statement as a string - :rtype: string + :rtype: string Compiles an ``INSERT`` statement and returns it as a string. @@ -1847,7 +1847,7 @@ Class Reference :param bool $reset: Whether to reset the current QB values or not :returns: The compiled SQL statement as a string - :rtype: string + :rtype: string Compiles an ``UPDATE`` statement and returns it as a string. @@ -1855,6 +1855,6 @@ Class Reference :param bool $reset: Whether to reset the current QB values or not :returns: The compiled SQL statement as a string - :rtype: string + :rtype: string Compiles a ``DELETE`` statement and returns it as a string. diff --git a/user_guide_src/source/helpers/form_helper.rst b/user_guide_src/source/helpers/form_helper.rst index f54b1166a05e..685c7e7041ea 100644 --- a/user_guide_src/source/helpers/form_helper.rst +++ b/user_guide_src/source/helpers/form_helper.rst @@ -48,11 +48,11 @@ The following functions are available: .. php:function:: form_open([$action = ''[, $attributes = ''[, $hidden = []]]]) - :param string $action: Form action/target URI string - :param mixed $attributes: HTML attributes, as an array or escaped string - :param array $hidden: An array of hidden fields' definitions - :returns: An HTML form opening tag - :rtype: string + :param string $action: Form action/target URI string + :param mixed $attributes: HTML attributes, as an array or escaped string + :param array $hidden: An array of hidden fields' definitions + :returns: An HTML form opening tag + :rtype: string Creates an opening form tag with a site URL **built from your config preferences**. It will optionally let you add form attributes and hidden input fields, and @@ -123,11 +123,11 @@ The following functions are available: .. php:function:: form_open_multipart([$action = ''[, $attributes = ''[, $hidden = []]]]) - :param string $action: Form action/target URI string - :param mixed $attributes: HTML attributes, as an array or escaped string - :param array $hidden: An array of hidden fields' definitions - :returns: An HTML multipart form opening tag - :rtype: string + :param string $action: Form action/target URI string + :param mixed $attributes: HTML attributes, as an array or escaped string + :param array $hidden: An array of hidden fields' definitions + :returns: An HTML multipart form opening tag + :rtype: string This function is identical to :php:func:`form_open()` above, except that it adds a *multipart* attribute, which is necessary if you @@ -135,10 +135,10 @@ The following functions are available: .. php:function:: form_hidden($name[, $value = '']) - :param string $name: Field name - :param string $value: Field value - :returns: An HTML hidden input field tag - :rtype: string + :param string $name: Field name + :param string $value: Field value + :returns: An HTML hidden input field tag + :rtype: string Lets you generate hidden input fields. You can either submit a name/value string to create one field:: @@ -202,12 +202,12 @@ The following functions are available: .. php:function:: form_input([$data = ''[, $value = ''[, $extra = ''[, $type = 'text']]]]) - :param array $data: Field attributes data - :param string $value: Field value - :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :param array $data: Field attributes data + :param string $value: Field value + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string :param string $type: The type of input field. i.e., 'text', 'email', 'number', etc. - :returns: An HTML text input field tag - :rtype: string + :returns: An HTML text input field tag + :rtype: string Lets you generate a standard text input field. You can minimally pass the field name and value in the first and second parameter:: @@ -257,22 +257,22 @@ The following functions are available: .. php:function:: form_password([$data = ''[, $value = ''[, $extra = '']]]) - :param array $data: Field attributes data - :param string $value: Field value - :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string - :returns: An HTML password input field tag - :rtype: string + :param array $data: Field attributes data + :param string $value: Field value + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML password input field tag + :rtype: string This function is identical in all respects to the :php:func:`form_input()` function above except that it uses the "password" input type. .. php:function:: form_upload([$data = ''[, $value = ''[, $extra = '']]]) - :param array $data: Field attributes data - :param string $value: Field value - :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string - :returns: An HTML file upload input field tag - :rtype: string + :param array $data: Field attributes data + :param string $value: Field value + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML file upload input field tag + :rtype: string This function is identical in all respects to the :php:func:`form_input()` function above except that it uses the "file" input type, allowing it to @@ -280,11 +280,11 @@ The following functions are available: .. php:function:: form_textarea([$data = ''[, $value = ''[, $extra = '']]]) - :param array $data: Field attributes data - :param string $value: Field value - :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string - :returns: An HTML textarea tag - :rtype: string + :param array $data: Field attributes data + :param string $value: Field value + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML textarea tag + :rtype: string This function is identical in all respects to the :php:func:`form_input()` function above except that it generates a "textarea" type. @@ -294,12 +294,12 @@ The following functions are available: .. php:function:: form_dropdown([$name = ''[, $options = [][, $selected = [][, $extra = '']]]]) - :param string $name: Field name - :param array $options: An associative array of options to be listed - :param array $selected: List of fields to mark with the *selected* attribute - :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string - :returns: An HTML dropdown select field tag - :rtype: string + :param string $name: Field name + :param array $options: An associative array of options to be listed + :param array $selected: List of fields to mark with the *selected* attribute + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML dropdown select field tag + :rtype: string Lets you create a standard drop-down field. The first parameter will contain the name of the field, the second parameter will contain an @@ -365,12 +365,12 @@ The following functions are available: .. php:function:: form_multiselect([$name = ''[, $options = [][, $selected = [][, $extra = '']]]]) - :param string $name: Field name - :param array $options: An associative array of options to be listed - :param array $selected: List of fields to mark with the *selected* attribute - :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string - :returns: An HTML dropdown multiselect field tag - :rtype: string + :param string $name: Field name + :param array $options: An associative array of options to be listed + :param array $selected: List of fields to mark with the *selected* attribute + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML dropdown multiselect field tag + :rtype: string Lets you create a standard multiselect field. The first parameter will contain the name of the field, the second parameter will contain an @@ -383,10 +383,10 @@ The following functions are available: .. php:function:: form_fieldset([$legend_text = ''[, $attributes = []]]) - :param string $legend_text: Text to put in the tag - :param array $attributes: Attributes to be set on the
    tag - :returns: An HTML fieldset opening tag - :rtype: string + :param string $legend_text: Text to put in the tag + :param array $attributes: Attributes to be set on the
    tag + :returns: An HTML fieldset opening tag + :rtype: string Lets you generate fieldset/legend fields. @@ -409,8 +409,8 @@ The following functions are available: second parameter if you prefer to set additional attributes:: $attributes = [ - 'id' => 'address_info', - 'class' => 'address_info' + 'id' => 'address_info', + 'class' => 'address_info' ]; echo form_fieldset('Address Information', $attributes); @@ -428,9 +428,9 @@ The following functions are available: .. php:function:: form_fieldset_close([$extra = '']) - :param string $extra: Anything to append after the closing tag, *as is* - :returns: An HTML fieldset closing tag - :rtype: string + :param string $extra: Anything to append after the closing tag, *as is* + :returns: An HTML fieldset closing tag + :rtype: string Produces a closing
    tag. The only advantage to using this function is it permits you to pass data to it which will be added below @@ -444,12 +444,12 @@ The following functions are available: .. php:function:: form_checkbox([$data = ''[, $value = ''[, $checked = false[, $extra = '']]]]) - :param array $data: Field attributes data - :param string $value: Field value - :param bool $checked: Whether to mark the checkbox as being *checked* - :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string - :returns: An HTML checkbox input tag - :rtype: string + :param array $data: Field attributes data + :param string $value: Field value + :param bool $checked: Whether to mark the checkbox as being *checked* + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML checkbox input tag + :rtype: string Lets you generate a checkbox field. Simple example:: @@ -487,23 +487,23 @@ The following functions are available: .. php:function:: form_radio([$data = ''[, $value = ''[, $checked = false[, $extra = '']]]]) - :param array $data: Field attributes data - :param string $value: Field value - :param bool $checked: Whether to mark the radio button as being *checked* - :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string - :returns: An HTML radio input tag - :rtype: string + :param array $data: Field attributes data + :param string $value: Field value + :param bool $checked: Whether to mark the radio button as being *checked* + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML radio input tag + :rtype: string This function is identical in all respects to the :php:func:`form_checkbox()` function above except that it uses the "radio" input type. .. php:function:: form_label([$label_text = ''[, $id = ''[, $attributes = []]]]) - :param string $label_text: Text to put in the
    {queries} - + From 027e41ba4a7441c201c368cafba67f937262a35f Mon Sep 17 00:00:00 2001 From: Daniel Tiringer Date: Fri, 8 Oct 2021 13:47:48 +0200 Subject: [PATCH 284/490] Add css class to mark duplicates --- system/Debug/Toolbar/Views/toolbar.css | 67 ++++++++------------------ 1 file changed, 20 insertions(+), 47 deletions(-) diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index 7382f4d7e60a..f0a5271535d4 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -1,10 +1,9 @@ -/** - * This file is part of the CodeIgniter 4 framework. - * - * (c) CodeIgniter Foundation - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. +/*! CodeIgniter 4 - Debug bar + * ============================================================================ + * Forum: https://forum.codeigniter.com + * Github: https://github.com/codeigniter4/codeigniter4 + * Slack: https://codeigniterchat.slack.com + * Website: https://codeigniter.com */ #debug-icon { bottom: 0; @@ -118,6 +117,7 @@ overflow: hidden; overflow-y: auto; padding: 0 12px 0 12px; + /* give room for OS X scrollbar */ white-space: nowrap; z-index: 10000; } #debug-bar.fixed-top { @@ -200,14 +200,7 @@ padding: 5px; position: relative; } #debug-bar .timeline td:first-child { - border-left: 0; - max-width: none; } - #debug-bar .timeline td.child-container { - padding: 0px; } - #debug-bar .timeline td.child-container .timeline { - margin: 0px; } - #debug-bar .timeline td.child-container .timeline td:first-child:not(.child-container) { - padding-left: calc(5px + 10px * var(--level)); } + border-left: 0; } #debug-bar .timeline .timer { border-radius: 4px; -moz-border-radius: 4px; @@ -216,22 +209,6 @@ padding: 5px; position: absolute; top: 30%; } - #debug-bar .timeline .timeline-parent { - cursor: pointer; } - #debug-bar .timeline .timeline-parent td:first-child nav { - background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent; - background-position: 0 25%; - display: inline-block; - height: 15px; - width: 15px; - margin-right: 3px; - vertical-align: middle; } - #debug-bar .timeline .timeline-parent-open { - background-color: #DFDFDF; } - #debug-bar .timeline .timeline-parent-open td:first-child nav { - background-position: 0 75%; } - #debug-bar .timeline .child-row:hover { - background: transparent; } #debug-bar .route-params, #debug-bar .route-params-item { vertical-align: top; } @@ -267,9 +244,7 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #debug-icon a:active, - #debug-icon a:link, - #debug-icon a:visited { + #debug-icon a:active, #debug-icon a:link, #debug-icon a:visited { color: #DD8615; } #debug-bar { @@ -292,7 +267,7 @@ #debug-bar button { background-color: #FFFFFF; } #debug-bar table strong { - color: #DD8615; } + color: #FDC894; } #debug-bar table tbody tr:hover { background-color: #DFDFDF; } #debug-bar table tbody tr.current { @@ -355,9 +330,7 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #debug-icon a:active, - #debug-icon a:link, - #debug-icon a:visited { + #debug-icon a:active, #debug-icon a:link, #debug-icon a:visited { color: #DD8615; } #debug-bar { background-color: #252525; @@ -379,7 +352,7 @@ #debug-bar button { background-color: #252525; } #debug-bar table strong { - color: #DD8615; } + color: #FDC894; } #debug-bar table tbody tr:hover { background-color: #434343; } #debug-bar table tbody tr.current { @@ -441,9 +414,7 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #toolbarContainer.dark #debug-icon a:active, - #toolbarContainer.dark #debug-icon a:link, - #toolbarContainer.dark #debug-icon a:visited { + #toolbarContainer.dark #debug-icon a:active, #toolbarContainer.dark #debug-icon a:link, #toolbarContainer.dark #debug-icon a:visited { color: #DD8615; } #toolbarContainer.dark #debug-bar { @@ -466,7 +437,7 @@ #toolbarContainer.dark #debug-bar button { background-color: #252525; } #toolbarContainer.dark #debug-bar table strong { - color: #DD8615; } + color: #FDC894; } #toolbarContainer.dark #debug-bar table tbody tr:hover { background-color: #434343; } #toolbarContainer.dark #debug-bar table tbody tr.current { @@ -534,9 +505,7 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #toolbarContainer.light #debug-icon a:active, - #toolbarContainer.light #debug-icon a:link, - #toolbarContainer.light #debug-icon a:visited { + #toolbarContainer.light #debug-icon a:active, #toolbarContainer.light #debug-icon a:link, #toolbarContainer.light #debug-icon a:visited { color: #DD8615; } #toolbarContainer.light #debug-bar { @@ -559,7 +528,7 @@ #toolbarContainer.light #debug-bar button { background-color: #FFFFFF; } #toolbarContainer.light #debug-bar table strong { - color: #DD8615; } + color: #FDC894; } #toolbarContainer.light #debug-bar table tbody tr:hover { background-color: #DFDFDF; } #toolbarContainer.light #debug-bar table tbody tr.current { @@ -645,3 +614,7 @@ .debug-bar-noverflow { overflow: hidden; } + +#ci-database table tbody tr.duplicate { + background-color: #DD4814; +} From 4ea6493f4cd41b459419528056ebd86c0cb8b029 Mon Sep 17 00:00:00 2001 From: Daniel Tiringer Date: Fri, 8 Oct 2021 13:56:41 +0200 Subject: [PATCH 285/490] Correct the accidentally modified css --- system/Debug/Toolbar/Views/toolbar.css | 66 +++++++++++++++++++------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index f0a5271535d4..5f971ba7172a 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -1,9 +1,10 @@ -/*! CodeIgniter 4 - Debug bar - * ============================================================================ - * Forum: https://forum.codeigniter.com - * Github: https://github.com/codeigniter4/codeigniter4 - * Slack: https://codeigniterchat.slack.com - * Website: https://codeigniter.com +/** + * This file is part of the CodeIgniter 4 framework. + * + * (c) CodeIgniter Foundation + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ #debug-icon { bottom: 0; @@ -117,7 +118,6 @@ overflow: hidden; overflow-y: auto; padding: 0 12px 0 12px; - /* give room for OS X scrollbar */ white-space: nowrap; z-index: 10000; } #debug-bar.fixed-top { @@ -200,7 +200,14 @@ padding: 5px; position: relative; } #debug-bar .timeline td:first-child { - border-left: 0; } + border-left: 0; + max-width: none; } + #debug-bar .timeline td.child-container { + padding: 0px; } + #debug-bar .timeline td.child-container .timeline { + margin: 0px; } + #debug-bar .timeline td.child-container .timeline td:first-child:not(.child-container) { + padding-left: calc(5px + 10px * var(--level)); } #debug-bar .timeline .timer { border-radius: 4px; -moz-border-radius: 4px; @@ -209,6 +216,22 @@ padding: 5px; position: absolute; top: 30%; } + #debug-bar .timeline .timeline-parent { + cursor: pointer; } + #debug-bar .timeline .timeline-parent td:first-child nav { + background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent; + background-position: 0 25%; + display: inline-block; + height: 15px; + width: 15px; + margin-right: 3px; + vertical-align: middle; } + #debug-bar .timeline .timeline-parent-open { + background-color: #DFDFDF; } + #debug-bar .timeline .timeline-parent-open td:first-child nav { + background-position: 0 75%; } + #debug-bar .timeline .child-row:hover { + background: transparent; } #debug-bar .route-params, #debug-bar .route-params-item { vertical-align: top; } @@ -244,7 +267,9 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #debug-icon a:active, #debug-icon a:link, #debug-icon a:visited { + #debug-icon a:active, + #debug-icon a:link, + #debug-icon a:visited { color: #DD8615; } #debug-bar { @@ -267,7 +292,7 @@ #debug-bar button { background-color: #FFFFFF; } #debug-bar table strong { - color: #FDC894; } + color: #DD8615; } #debug-bar table tbody tr:hover { background-color: #DFDFDF; } #debug-bar table tbody tr.current { @@ -330,7 +355,9 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #debug-icon a:active, #debug-icon a:link, #debug-icon a:visited { + #debug-icon a:active, + #debug-icon a:link, + #debug-icon a:visited { color: #DD8615; } #debug-bar { background-color: #252525; @@ -352,7 +379,7 @@ #debug-bar button { background-color: #252525; } #debug-bar table strong { - color: #FDC894; } + color: #DD8615; } #debug-bar table tbody tr:hover { background-color: #434343; } #debug-bar table tbody tr.current { @@ -414,7 +441,9 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #toolbarContainer.dark #debug-icon a:active, #toolbarContainer.dark #debug-icon a:link, #toolbarContainer.dark #debug-icon a:visited { + #toolbarContainer.dark #debug-icon a:active, + #toolbarContainer.dark #debug-icon a:link, + #toolbarContainer.dark #debug-icon a:visited { color: #DD8615; } #toolbarContainer.dark #debug-bar { @@ -437,7 +466,7 @@ #toolbarContainer.dark #debug-bar button { background-color: #252525; } #toolbarContainer.dark #debug-bar table strong { - color: #FDC894; } + color: #DD8615; } #toolbarContainer.dark #debug-bar table tbody tr:hover { background-color: #434343; } #toolbarContainer.dark #debug-bar table tbody tr.current { @@ -505,7 +534,9 @@ box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } - #toolbarContainer.light #debug-icon a:active, #toolbarContainer.light #debug-icon a:link, #toolbarContainer.light #debug-icon a:visited { + #toolbarContainer.light #debug-icon a:active, + #toolbarContainer.light #debug-icon a:link, + #toolbarContainer.light #debug-icon a:visited { color: #DD8615; } #toolbarContainer.light #debug-bar { @@ -528,7 +559,7 @@ #toolbarContainer.light #debug-bar button { background-color: #FFFFFF; } #toolbarContainer.light #debug-bar table strong { - color: #FDC894; } + color: #DD8615; } #toolbarContainer.light #debug-bar table tbody tr:hover { background-color: #DFDFDF; } #toolbarContainer.light #debug-bar table tbody tr.current { @@ -616,5 +647,4 @@ overflow: hidden; } #ci-database table tbody tr.duplicate { - background-color: #DD4814; -} + background-color: #DD4814;} From 674af492e9c9d07719fb73f86b984e606a60619f Mon Sep 17 00:00:00 2001 From: Daniel Tiringer Date: Fri, 8 Oct 2021 23:15:49 +0200 Subject: [PATCH 286/490] Use the hover colors to highlight duplicates --- system/Debug/Toolbar/Views/toolbar.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index 5f971ba7172a..b5a223b55d70 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -471,6 +471,8 @@ background-color: #434343; } #toolbarContainer.dark #debug-bar table tbody tr.current { background-color: #FDC894; } + #toolbarContainer.dark #ci-database table tbody tr.duplicate { + background-color: #434343;} #toolbarContainer.dark #debug-bar table tbody tr.current td { color: #252525; } #toolbarContainer.dark #debug-bar table tbody tr.current:hover td { @@ -564,6 +566,8 @@ background-color: #DFDFDF; } #toolbarContainer.light #debug-bar table tbody tr.current { background-color: #FDC894; } + #toolbarContainer.light #ci-database table tbody tr.duplicate { + background-color: #DFDFDF;} #toolbarContainer.light #debug-bar table tbody tr.current:hover td { background-color: #DD4814; color: #FFFFFF; } @@ -645,6 +649,3 @@ .debug-bar-noverflow { overflow: hidden; } - -#ci-database table tbody tr.duplicate { - background-color: #DD4814;} From b2560d56c5b0b048ba73d872be8340c78285af52 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 10 Oct 2021 22:15:35 +0900 Subject: [PATCH 287/490] docs: move PULL_REQUEST_TEMPLATE.md to .github/ --- PULL_REQUEST_TEMPLATE.md => .github/PULL_REQUEST_TEMPLATE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename PULL_REQUEST_TEMPLATE.md => .github/PULL_REQUEST_TEMPLATE.md (100%) diff --git a/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md From 7f88eceabbcf0cb128992ffe27cc77d6abec5deb Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 10 Oct 2021 22:16:54 +0900 Subject: [PATCH 288/490] docs: replace PHPdoc with PHPDoc --- .github/PULL_REQUEST_TEMPLATE.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index db5aeb9eca37..eb50d54c4c4f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,7 +5,7 @@ Explain what you have changed, and why. **Checklist:** - [ ] Securely signed commits -- [ ] Component(s) with PHPdocs +- [ ] Component(s) with PHPDocs - [ ] Unit testing, with >80% coverage - [ ] User guide updated - [ ] Conforms to style guide @@ -19,4 +19,3 @@ Explain what you have changed, and why. - Unsolicited pull requests will be considered, but there is no guarantee of acceptance - Pull requests should be from a feature branch in the contributor's fork of the repository to the develop branch of the project repository - From aaab1bc0509d0677044d8175ed888bb696be515e Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 11 Oct 2021 11:29:47 +0900 Subject: [PATCH 289/490] docs: update expression Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com> --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index eb50d54c4c4f..6204913fd437 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,7 +5,7 @@ Explain what you have changed, and why. **Checklist:** - [ ] Securely signed commits -- [ ] Component(s) with PHPDocs +- [ ] Component(s) with PHPDoc blocks, only if necessary or adds value - [ ] Unit testing, with >80% coverage - [ ] User guide updated - [ ] Conforms to style guide From cb714b5b0c8c197e13c0373f5ac1221c549305aa Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Mon, 11 Oct 2021 17:03:05 +0800 Subject: [PATCH 290/490] Change behavior of `class_attributes_separation` rule --- .no-header.php-cs-fixer.dist.php | 24 ++++++---- .php-cs-fixer.dist.php | 24 ++++++---- .../tests/_support/Models/ExampleModel.php | 12 ++--- .../tests/_support/Models/ExampleModel.php | 12 ++--- app/Config/Kint.php | 24 +++------- system/Test/Mock/MockAppConfig.php | 44 ++++++++----------- system/Test/Mock/MockAutoload.php | 3 +- system/Test/Mock/MockCLIConfig.php | 40 +++++++---------- system/Test/Mock/MockCURLRequest.php | 1 - system/Test/Mock/MockConnection.php | 2 - system/Test/Mock/MockServices.php | 1 - tests/_support/Models/EntityModel.php | 15 +++---- tests/_support/Models/EventModel.php | 13 ++---- tests/_support/Models/FabricatorModel.php | 15 +++---- tests/_support/Models/JobModel.php | 16 +++---- tests/_support/Models/SecondaryModel.php | 15 +++---- tests/_support/Models/UserModel.php | 19 +++----- tests/_support/Models/ValidErrorsModel.php | 13 ++---- tests/_support/Models/ValidModel.php | 14 ++---- .../Models/WithoutAutoIncrementModel.php | 7 +-- .../Commands/EnvironmentCommandTest.php | 4 +- .../Commands/MigrationIntegrationTest.php | 4 +- tests/system/Config/fixtures/Encryption.php | 1 + tests/system/Config/fixtures/SimpleConfig.php | 4 +- tests/system/ControllerTest.php | 1 + tests/system/Database/BaseConnectionTest.php | 1 - tests/system/Database/ConfigTest.php | 3 -- tests/system/Database/Live/AliasTest.php | 3 +- tests/system/Database/Live/BadQueryTest.php | 3 +- tests/system/Database/Live/ConnectTest.php | 2 - tests/system/Database/Live/CountTest.php | 3 +- .../Live/DatabaseTestTraitCaseTest.php | 3 +- tests/system/Database/Live/DeleteTest.php | 3 +- tests/system/Database/Live/EmptyTest.php | 3 +- tests/system/Database/Live/EscapeTest.php | 1 - tests/system/Database/Live/ForgeTest.php | 3 +- tests/system/Database/Live/FromTest.php | 3 +- tests/system/Database/Live/GetNumRowsTest.php | 3 +- tests/system/Database/Live/GetTest.php | 3 +- tests/system/Database/Live/GroupTest.php | 3 +- tests/system/Database/Live/IncrementTest.php | 3 +- tests/system/Database/Live/InsertTest.php | 3 +- tests/system/Database/Live/JoinTest.php | 3 +- tests/system/Database/Live/LikeTest.php | 3 +- tests/system/Database/Live/LimitTest.php | 4 +- tests/system/Database/Live/OrderTest.php | 3 +- tests/system/Database/Live/SelectTest.php | 3 +- tests/system/Database/Live/UpdateTest.php | 3 +- tests/system/Database/Live/WhereTest.php | 3 +- .../Database/Live/WriteTypeQueryTest.php | 3 +- .../Migrations/MigrationRunnerTest.php | 1 - tests/system/Entity/EntityTest.php | 9 ---- tests/system/HTTP/RedirectResponseTest.php | 1 + tests/system/Models/InsertModelTest.php | 2 - tests/system/Models/SaveModelTest.php | 1 - tests/system/Models/UpdateModelTest.php | 3 -- tests/system/Pager/PagerTest.php | 1 + .../Session/Handlers/DatabaseHandlerTest.php | 3 +- .../system/Validation/CreditCardRulesTest.php | 1 + tests/system/Validation/FileRulesTest.php | 1 + tests/system/Validation/FormatRulesTest.php | 1 + tests/system/Validation/RulesTest.php | 1 + tests/system/Validation/ValidationTest.php | 1 + tests/system/View/ParserPluginTest.php | 1 + 64 files changed, 161 insertions(+), 265 deletions(-) diff --git a/.no-header.php-cs-fixer.dist.php b/.no-header.php-cs-fixer.dist.php index 6e0a5c09602d..a3030aa98f58 100644 --- a/.no-header.php-cs-fixer.dist.php +++ b/.no-header.php-cs-fixer.dist.php @@ -30,14 +30,22 @@ $overrides = [ // @TODO Remove once these are live in coding-standard 'assign_null_coalescing_to_coalesce_equal' => false, // requires 7.4+ - 'control_structure_continuation_position' => ['position' => 'same_line'], - 'empty_loop_condition' => ['style' => 'while'], - 'integer_literal_case' => true, - 'modernize_strpos' => false, // requires 8.0+ - 'no_alternative_syntax' => ['fix_non_monolithic_code' => false], - 'no_space_around_double_colon' => true, - 'octal_notation' => false, // requires 8.1+ - 'string_length_to_empty' => true, + 'class_attributes_separation' => [ + 'elements' => [ + 'const' => 'none', + 'property' => 'none', + 'method' => 'one', + 'trait_import' => 'none', + ], + ], + 'control_structure_continuation_position' => ['position' => 'same_line'], + 'empty_loop_condition' => ['style' => 'while'], + 'integer_literal_case' => true, + 'modernize_strpos' => false, // requires 8.0+ + 'no_alternative_syntax' => ['fix_non_monolithic_code' => false], + 'no_space_around_double_colon' => true, + 'octal_notation' => false, // requires 8.1+ + 'string_length_to_empty' => true, ]; $options = [ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 2196640dae93..743062d44a88 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -37,14 +37,22 @@ $overrides = [ // @TODO Remove once these are live in coding-standard 'assign_null_coalescing_to_coalesce_equal' => false, // requires 7.4+ - 'control_structure_continuation_position' => ['position' => 'same_line'], - 'empty_loop_condition' => ['style' => 'while'], - 'integer_literal_case' => true, - 'modernize_strpos' => false, // requires 8.0+ - 'no_alternative_syntax' => ['fix_non_monolithic_code' => false], - 'no_space_around_double_colon' => true, - 'octal_notation' => false, // requires 8.1+ - 'string_length_to_empty' => true, + 'class_attributes_separation' => [ + 'elements' => [ + 'const' => 'none', + 'property' => 'none', + 'method' => 'one', + 'trait_import' => 'none', + ], + ], + 'control_structure_continuation_position' => ['position' => 'same_line'], + 'empty_loop_condition' => ['style' => 'while'], + 'integer_literal_case' => true, + 'modernize_strpos' => false, // requires 8.0+ + 'no_alternative_syntax' => ['fix_non_monolithic_code' => false], + 'no_space_around_double_colon' => true, + 'octal_notation' => false, // requires 8.1+ + 'string_length_to_empty' => true, ]; $options = [ diff --git a/admin/module/tests/_support/Models/ExampleModel.php b/admin/module/tests/_support/Models/ExampleModel.php index a71009e74f8b..f0687e9b1985 100644 --- a/admin/module/tests/_support/Models/ExampleModel.php +++ b/admin/module/tests/_support/Models/ExampleModel.php @@ -6,22 +6,18 @@ class ExampleModel extends Model { - protected $table = 'factories'; - protected $primaryKey = 'id'; - + protected $table = 'factories'; + protected $primaryKey = 'id'; protected $returnType = 'object'; protected $useSoftDeletes = false; - - protected $allowedFields = [ + protected $allowedFields = [ 'name', 'uid', 'class', 'icon', 'summary', ]; - - protected $useTimestamps = true; - + protected $useTimestamps = true; protected $validationRules = []; protected $validationMessages = []; protected $skipValidation = false; diff --git a/admin/starter/tests/_support/Models/ExampleModel.php b/admin/starter/tests/_support/Models/ExampleModel.php index a71009e74f8b..f0687e9b1985 100644 --- a/admin/starter/tests/_support/Models/ExampleModel.php +++ b/admin/starter/tests/_support/Models/ExampleModel.php @@ -6,22 +6,18 @@ class ExampleModel extends Model { - protected $table = 'factories'; - protected $primaryKey = 'id'; - + protected $table = 'factories'; + protected $primaryKey = 'id'; protected $returnType = 'object'; protected $useSoftDeletes = false; - - protected $allowedFields = [ + protected $allowedFields = [ 'name', 'uid', 'class', 'icon', 'summary', ]; - - protected $useTimestamps = true; - + protected $useTimestamps = true; protected $validationRules = []; protected $validationMessages = []; protected $skipValidation = false; diff --git a/app/Config/Kint.php b/app/Config/Kint.php index 4b52422af7d7..b1016ed57923 100644 --- a/app/Config/Kint.php +++ b/app/Config/Kint.php @@ -24,26 +24,19 @@ class Kint extends BaseConfig */ public $plugins; - - public $maxDepth = 6; - + public $maxDepth = 6; public $displayCalledFrom = true; - - public $expanded = false; + public $expanded = false; /* |-------------------------------------------------------------------------- | RichRenderer Settings |-------------------------------------------------------------------------- */ - public $richTheme = 'aante-light.css'; - + public $richTheme = 'aante-light.css'; public $richFolder = false; - - public $richSort = Renderer::SORT_FULL; - + public $richSort = Renderer::SORT_FULL; public $richObjectPlugins; - public $richTabPlugins; /* @@ -51,11 +44,8 @@ class Kint extends BaseConfig | CLI Settings |-------------------------------------------------------------------------- */ - public $cliColors = true; - - public $cliForceUTF8 = false; - + public $cliColors = true; + public $cliForceUTF8 = false; public $cliDetectWidth = true; - - public $cliMinWidth = 40; + public $cliMinWidth = 40; } diff --git a/system/Test/Mock/MockAppConfig.php b/system/Test/Mock/MockAppConfig.php index 26fb350db12c..fe51c4d3202f 100644 --- a/system/Test/Mock/MockAppConfig.php +++ b/system/Test/Mock/MockAppConfig.php @@ -15,31 +15,25 @@ class MockAppConfig extends App { - public $baseURL = 'http://example.com/'; - - public $uriProtocol = 'REQUEST_URI'; - - public $cookiePrefix = ''; - public $cookieDomain = ''; - public $cookiePath = '/'; - public $cookieSecure = false; - public $cookieHTTPOnly = false; - public $cookieSameSite = 'Lax'; - - public $proxyIPs = ''; - - public $CSRFProtection = false; - public $CSRFTokenName = 'csrf_test_name'; - public $CSRFHeaderName = 'X-CSRF-TOKEN'; - public $CSRFCookieName = 'csrf_cookie_name'; - public $CSRFExpire = 7200; - public $CSRFRegenerate = true; - public $CSRFExcludeURIs = ['http://example.com']; - public $CSRFRedirect = false; - public $CSRFSameSite = 'Lax'; - - public $CSPEnabled = false; - + public $baseURL = 'http://example.com/'; + public $uriProtocol = 'REQUEST_URI'; + public $cookiePrefix = ''; + public $cookieDomain = ''; + public $cookiePath = '/'; + public $cookieSecure = false; + public $cookieHTTPOnly = false; + public $cookieSameSite = 'Lax'; + public $proxyIPs = ''; + public $CSRFProtection = false; + public $CSRFTokenName = 'csrf_test_name'; + public $CSRFHeaderName = 'X-CSRF-TOKEN'; + public $CSRFCookieName = 'csrf_cookie_name'; + public $CSRFExpire = 7200; + public $CSRFRegenerate = true; + public $CSRFExcludeURIs = ['http://example.com']; + public $CSRFRedirect = false; + public $CSRFSameSite = 'Lax'; + public $CSPEnabled = false; public $defaultLocale = 'en'; public $negotiateLocale = false; public $supportedLocales = [ diff --git a/system/Test/Mock/MockAutoload.php b/system/Test/Mock/MockAutoload.php index 26a92b2f811c..291974c65be3 100644 --- a/system/Test/Mock/MockAutoload.php +++ b/system/Test/Mock/MockAutoload.php @@ -15,8 +15,7 @@ class MockAutoload extends Autoload { - public $psr4 = []; - + public $psr4 = []; public $classmap = []; public function __construct() diff --git a/system/Test/Mock/MockCLIConfig.php b/system/Test/Mock/MockCLIConfig.php index 5c6e6b5db195..0e5f9c8dd64e 100644 --- a/system/Test/Mock/MockCLIConfig.php +++ b/system/Test/Mock/MockCLIConfig.php @@ -15,29 +15,23 @@ class MockCLIConfig extends App { - public $baseURL = 'http://example.com/'; - - public $uriProtocol = 'REQUEST_URI'; - - public $cookiePrefix = ''; - public $cookieDomain = ''; - public $cookiePath = '/'; - public $cookieSecure = false; - public $cookieHTTPOnly = false; - public $cookieSameSite = 'Lax'; - - public $proxyIPs = ''; - - public $CSRFProtection = false; - public $CSRFTokenName = 'csrf_test_name'; - public $CSRFCookieName = 'csrf_cookie_name'; - public $CSRFExpire = 7200; - public $CSRFRegenerate = true; - public $CSRFExcludeURIs = ['http://example.com']; - public $CSRFSameSite = 'Lax'; - - public $CSPEnabled = false; - + public $baseURL = 'http://example.com/'; + public $uriProtocol = 'REQUEST_URI'; + public $cookiePrefix = ''; + public $cookieDomain = ''; + public $cookiePath = '/'; + public $cookieSecure = false; + public $cookieHTTPOnly = false; + public $cookieSameSite = 'Lax'; + public $proxyIPs = ''; + public $CSRFProtection = false; + public $CSRFTokenName = 'csrf_test_name'; + public $CSRFCookieName = 'csrf_cookie_name'; + public $CSRFExpire = 7200; + public $CSRFRegenerate = true; + public $CSRFExcludeURIs = ['http://example.com']; + public $CSRFSameSite = 'Lax'; + public $CSPEnabled = false; public $defaultLocale = 'en'; public $negotiateLocale = false; public $supportedLocales = [ diff --git a/system/Test/Mock/MockCURLRequest.php b/system/Test/Mock/MockCURLRequest.php index 5b7344fe13e5..635db366234c 100644 --- a/system/Test/Mock/MockCURLRequest.php +++ b/system/Test/Mock/MockCURLRequest.php @@ -23,7 +23,6 @@ class MockCURLRequest extends CURLRequest { public $curl_options; - protected $output = ''; public function setOutput($output) diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index 099a8b735d20..78bd405babad 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -19,9 +19,7 @@ class MockConnection extends BaseConnection { protected $returnValues = []; - public $database; - public $lastQuery; public function shouldReturn(string $method, $return) diff --git a/system/Test/Mock/MockServices.php b/system/Test/Mock/MockServices.php index f02404a1b74e..f18700426811 100644 --- a/system/Test/Mock/MockServices.php +++ b/system/Test/Mock/MockServices.php @@ -19,7 +19,6 @@ class MockServices extends BaseService public $psr4 = [ 'Tests/Support' => TESTPATH . '_support/', ]; - public $classmap = []; public function __construct() diff --git a/tests/_support/Models/EntityModel.php b/tests/_support/Models/EntityModel.php index fb53549e8fa6..f2bbe35ccfd1 100644 --- a/tests/_support/Models/EntityModel.php +++ b/tests/_support/Models/EntityModel.php @@ -15,17 +15,12 @@ class EntityModel extends Model { - protected $table = 'job'; - - protected $returnType = '\Tests\Support\Models\SimpleEntity'; - + protected $table = 'job'; + protected $returnType = '\Tests\Support\Models\SimpleEntity'; protected $useSoftDeletes = false; - - protected $dateFormat = 'int'; - - protected $deletedField = 'deleted_at'; - - protected $allowedFields = [ + protected $dateFormat = 'int'; + protected $deletedField = 'deleted_at'; + protected $allowedFields = [ 'name', 'description', 'created_at', diff --git a/tests/_support/Models/EventModel.php b/tests/_support/Models/EventModel.php index 8b8a947efcc6..c3e0a145509b 100644 --- a/tests/_support/Models/EventModel.php +++ b/tests/_support/Models/EventModel.php @@ -15,21 +15,16 @@ class EventModel extends Model { - protected $table = 'user'; - - protected $returnType = 'array'; - + protected $table = 'user'; + protected $returnType = 'array'; protected $useSoftDeletes = false; - - protected $dateFormat = 'datetime'; - - protected $allowedFields = [ + protected $dateFormat = 'datetime'; + protected $allowedFields = [ 'name', 'email', 'country', 'deleted_at', ]; - protected $beforeInsert = ['beforeInsertMethod']; protected $afterInsert = ['afterInsertMethod']; protected $beforeUpdate = ['beforeUpdateMethod']; diff --git a/tests/_support/Models/FabricatorModel.php b/tests/_support/Models/FabricatorModel.php index 1c40e7e1464e..70efeeb5c942 100644 --- a/tests/_support/Models/FabricatorModel.php +++ b/tests/_support/Models/FabricatorModel.php @@ -16,17 +16,12 @@ class FabricatorModel extends Model { - protected $table = 'job'; - - protected $returnType = 'object'; - + protected $table = 'job'; + protected $returnType = 'object'; protected $useSoftDeletes = true; - - protected $useTimestamps = true; - - protected $dateFormat = 'int'; - - protected $allowedFields = [ + protected $useTimestamps = true; + protected $dateFormat = 'int'; + protected $allowedFields = [ 'name', 'description', ]; diff --git a/tests/_support/Models/JobModel.php b/tests/_support/Models/JobModel.php index 06b967af7a89..7f5044dddc61 100644 --- a/tests/_support/Models/JobModel.php +++ b/tests/_support/Models/JobModel.php @@ -15,20 +15,14 @@ class JobModel extends Model { - protected $table = 'job'; - - protected $returnType = 'object'; - + protected $table = 'job'; + protected $returnType = 'object'; protected $useSoftDeletes = false; - - protected $dateFormat = 'int'; - - protected $allowedFields = [ + protected $dateFormat = 'int'; + protected $allowedFields = [ 'name', 'description', ]; - - public $name = ''; - + public $name = ''; public $description = ''; } diff --git a/tests/_support/Models/SecondaryModel.php b/tests/_support/Models/SecondaryModel.php index b4ef155a5aa9..aff1c2646e14 100644 --- a/tests/_support/Models/SecondaryModel.php +++ b/tests/_support/Models/SecondaryModel.php @@ -15,17 +15,12 @@ class SecondaryModel extends Model { - protected $table = 'secondary'; - - protected $primaryKey = 'id'; - - protected $returnType = 'object'; - + protected $table = 'secondary'; + protected $primaryKey = 'id'; + protected $returnType = 'object'; protected $useSoftDeletes = false; - - protected $dateFormat = 'int'; - - protected $allowedFields = [ + protected $dateFormat = 'int'; + protected $allowedFields = [ 'key', 'value', ]; diff --git a/tests/_support/Models/UserModel.php b/tests/_support/Models/UserModel.php index 7e68c3441803..07c678fb5db5 100644 --- a/tests/_support/Models/UserModel.php +++ b/tests/_support/Models/UserModel.php @@ -15,24 +15,17 @@ class UserModel extends Model { - protected $table = 'user'; - + protected $table = 'user'; protected $allowedFields = [ 'name', 'email', 'country', 'deleted_at', ]; - - protected $returnType = 'object'; - + protected $returnType = 'object'; protected $useSoftDeletes = true; - - protected $dateFormat = 'datetime'; - - public $name = ''; - - public $email = ''; - - public $country = ''; + protected $dateFormat = 'datetime'; + public $name = ''; + public $email = ''; + public $country = ''; } diff --git a/tests/_support/Models/ValidErrorsModel.php b/tests/_support/Models/ValidErrorsModel.php index 04ff1a98f715..e05f801a6983 100644 --- a/tests/_support/Models/ValidErrorsModel.php +++ b/tests/_support/Models/ValidErrorsModel.php @@ -15,19 +15,14 @@ class ValidErrorsModel extends Model { - protected $table = 'job'; - - protected $returnType = 'object'; - + protected $table = 'job'; + protected $returnType = 'object'; protected $useSoftDeletes = false; - - protected $dateFormat = 'int'; - - protected $allowedFields = [ + protected $dateFormat = 'int'; + protected $allowedFields = [ 'name', 'description', ]; - protected $validationRules = [ 'name' => [ 'required', diff --git a/tests/_support/Models/ValidModel.php b/tests/_support/Models/ValidModel.php index 730985ad8142..216f80cc3f87 100644 --- a/tests/_support/Models/ValidModel.php +++ b/tests/_support/Models/ValidModel.php @@ -15,19 +15,14 @@ class ValidModel extends Model { - protected $table = 'job'; - - protected $returnType = 'object'; - + protected $table = 'job'; + protected $returnType = 'object'; protected $useSoftDeletes = false; - - protected $dateFormat = 'int'; - - protected $allowedFields = [ + protected $dateFormat = 'int'; + protected $allowedFields = [ 'name', 'description', ]; - protected $validationRules = [ 'name' => [ 'required', @@ -35,7 +30,6 @@ class ValidModel extends Model ], 'token' => 'permit_empty|in_list[{id}]', ]; - protected $validationMessages = [ 'name' => [ 'required' => 'You forgot to name the baby.', diff --git a/tests/_support/Models/WithoutAutoIncrementModel.php b/tests/_support/Models/WithoutAutoIncrementModel.php index 248fd3af1134..dcbb8c4aba3c 100644 --- a/tests/_support/Models/WithoutAutoIncrementModel.php +++ b/tests/_support/Models/WithoutAutoIncrementModel.php @@ -15,14 +15,11 @@ class WithoutAutoIncrementModel extends Model { - protected $table = 'without_auto_increment'; - - protected $primaryKey = 'key'; - + protected $table = 'without_auto_increment'; + protected $primaryKey = 'key'; protected $allowedFields = [ 'key', 'value', ]; - protected $useAutoIncrement = false; } diff --git a/tests/system/Commands/EnvironmentCommandTest.php b/tests/system/Commands/EnvironmentCommandTest.php index bd7f04ff3121..e81fe65ec622 100644 --- a/tests/system/Commands/EnvironmentCommandTest.php +++ b/tests/system/Commands/EnvironmentCommandTest.php @@ -20,9 +20,7 @@ final class EnvironmentCommandTest extends CIUnitTestCase { private $streamFilter; - - private $envPath = ROOTPATH . '.env'; - + private $envPath = ROOTPATH . '.env'; private $backupEnvPath = ROOTPATH . '.env.backup'; protected function setUp(): void diff --git a/tests/system/Commands/MigrationIntegrationTest.php b/tests/system/Commands/MigrationIntegrationTest.php index 77c047ef82ae..f2fca3405354 100644 --- a/tests/system/Commands/MigrationIntegrationTest.php +++ b/tests/system/Commands/MigrationIntegrationTest.php @@ -20,10 +20,8 @@ final class MigrationIntegrationTest extends CIUnitTestCase { private $streamFilter; - private $migrationFileFrom = SUPPORTPATH . 'Database/Migrations/20160428212500_Create_test_tables.php'; - - private $migrationFileTo = APPPATH . 'Database/Migrations/20160428212500_Create_test_tables.php'; + private $migrationFileTo = APPPATH . 'Database/Migrations/20160428212500_Create_test_tables.php'; protected function setUp(): void { diff --git a/tests/system/Config/fixtures/Encryption.php b/tests/system/Config/fixtures/Encryption.php index 2793795f56ff..7a8a00bc03fe 100644 --- a/tests/system/Config/fixtures/Encryption.php +++ b/tests/system/Config/fixtures/Encryption.php @@ -15,6 +15,7 @@ class Encryption extends EncryptionConfig { private const HEX2BIN = 'hex2bin:84cf2c0811d5daf9e1c897825a3debce91f9a33391e639f72f7a4740b30675a2'; private const BASE64 = 'base64:Psf8bUHRh1UJYG2M7e+5ec3MdjpKpzAr0twamcAvOcI='; + public $key; public $driver = 'MCrypt'; diff --git a/tests/system/Config/fixtures/SimpleConfig.php b/tests/system/Config/fixtures/SimpleConfig.php index d5d2665f657e..69f5590e90f1 100644 --- a/tests/system/Config/fixtures/SimpleConfig.php +++ b/tests/system/Config/fixtures/SimpleConfig.php @@ -25,7 +25,8 @@ class SimpleConfig extends \CodeIgniter\Config\BaseConfig public $simple = [ 'name' => null, ]; - // properties for environment over-ride testing + + // properties for environment override testing public $alpha = 'one'; public $bravo = 'two'; public $charlie = 'three'; @@ -41,7 +42,6 @@ class SimpleConfig extends \CodeIgniter\Config\BaseConfig 'doctor' => 'Bones', 'comms' => 'Uhuru', ]; - public $shortie; public $longie; public $onedeep_value; diff --git a/tests/system/ControllerTest.php b/tests/system/ControllerTest.php index 3d7c205eec0b..d4be0c503b7a 100644 --- a/tests/system/ControllerTest.php +++ b/tests/system/ControllerTest.php @@ -56,6 +56,7 @@ final class ControllerTest extends CIUnitTestCase * @var Response */ protected $response; + /** * @var LoggerInterface */ diff --git a/tests/system/Database/BaseConnectionTest.php b/tests/system/Database/BaseConnectionTest.php index 85126836650f..7d2352a0e9b2 100644 --- a/tests/system/Database/BaseConnectionTest.php +++ b/tests/system/Database/BaseConnectionTest.php @@ -39,7 +39,6 @@ final class BaseConnectionTest extends CIUnitTestCase 'strictOn' => true, 'failover' => [], ]; - protected $failoverOptions = [ 'DSN' => '', 'hostname' => 'localhost', diff --git a/tests/system/Database/ConfigTest.php b/tests/system/Database/ConfigTest.php index d4f795dcc26b..0dc9a1619485 100644 --- a/tests/system/Database/ConfigTest.php +++ b/tests/system/Database/ConfigTest.php @@ -40,7 +40,6 @@ final class ConfigTest extends CIUnitTestCase 'failover' => [], 'port' => 3306, ]; - protected $dsnGroup = [ 'DSN' => 'MySQLi://user:pass@localhost:3306/dbname?DBPrefix=test_&pConnect=true&charset=latin1&DBCollat=latin1_swedish_ci', 'hostname' => '', @@ -60,7 +59,6 @@ final class ConfigTest extends CIUnitTestCase 'failover' => [], 'port' => 3306, ]; - protected $dsnGroupPostgre = [ 'DSN' => 'Postgre://user:pass@localhost:5432/dbname?DBPrefix=test_&connect_timeout=5&sslmode=1', 'hostname' => '', @@ -80,7 +78,6 @@ final class ConfigTest extends CIUnitTestCase 'failover' => [], 'port' => 5432, ]; - protected $dsnGroupPostgreNative = [ 'DSN' => 'pgsql:host=localhost;port=5432;dbname=database_name', 'hostname' => '', diff --git a/tests/system/Database/Live/AliasTest.php b/tests/system/Database/Live/AliasTest.php index 0024625ce5b5..c032cd1b6546 100644 --- a/tests/system/Database/Live/AliasTest.php +++ b/tests/system/Database/Live/AliasTest.php @@ -24,8 +24,7 @@ final class AliasTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testAlias() { diff --git a/tests/system/Database/Live/BadQueryTest.php b/tests/system/Database/Live/BadQueryTest.php index 44a2adb0a391..b7d7840525c6 100644 --- a/tests/system/Database/Live/BadQueryTest.php +++ b/tests/system/Database/Live/BadQueryTest.php @@ -25,8 +25,7 @@ final class BadQueryTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; protected static $origDebug; /** diff --git a/tests/system/Database/Live/ConnectTest.php b/tests/system/Database/Live/ConnectTest.php index 680f03c94ca7..5ee18a6f25eb 100644 --- a/tests/system/Database/Live/ConnectTest.php +++ b/tests/system/Database/Live/ConnectTest.php @@ -27,9 +27,7 @@ final class ConnectTest extends CIUnitTestCase use DatabaseTestTrait; protected $group1; - protected $group2; - protected $tests; protected function setUp(): void diff --git a/tests/system/Database/Live/CountTest.php b/tests/system/Database/Live/CountTest.php index bfe917cb2adc..d87833759f24 100644 --- a/tests/system/Database/Live/CountTest.php +++ b/tests/system/Database/Live/CountTest.php @@ -24,8 +24,7 @@ final class CountTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testCountReturnsZeroWithNoResults() { diff --git a/tests/system/Database/Live/DatabaseTestTraitCaseTest.php b/tests/system/Database/Live/DatabaseTestTraitCaseTest.php index 8e917a7d30e9..3e9267b2b05d 100644 --- a/tests/system/Database/Live/DatabaseTestTraitCaseTest.php +++ b/tests/system/Database/Live/DatabaseTestTraitCaseTest.php @@ -24,8 +24,7 @@ final class DatabaseTestTraitCaseTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testHasInDatabase() { diff --git a/tests/system/Database/Live/DeleteTest.php b/tests/system/Database/Live/DeleteTest.php index bd3cd7d6d05c..31b89412e78a 100644 --- a/tests/system/Database/Live/DeleteTest.php +++ b/tests/system/Database/Live/DeleteTest.php @@ -25,8 +25,7 @@ final class DeleteTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testDeleteThrowExceptionWithNoCriteria() { diff --git a/tests/system/Database/Live/EmptyTest.php b/tests/system/Database/Live/EmptyTest.php index 5dfcec87e418..912f627911c6 100644 --- a/tests/system/Database/Live/EmptyTest.php +++ b/tests/system/Database/Live/EmptyTest.php @@ -24,8 +24,7 @@ final class EmptyTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testEmpty() { diff --git a/tests/system/Database/Live/EscapeTest.php b/tests/system/Database/Live/EscapeTest.php index f5c3ff8b59c1..08265788d00e 100644 --- a/tests/system/Database/Live/EscapeTest.php +++ b/tests/system/Database/Live/EscapeTest.php @@ -24,7 +24,6 @@ final class EscapeTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = false; - protected $char; protected function setUp(): void diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index d459bc92e391..1f8d8108d42d 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -29,8 +29,7 @@ final class ForgeTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; /** * @var Forge diff --git a/tests/system/Database/Live/FromTest.php b/tests/system/Database/Live/FromTest.php index 6a17f800e55a..5037dd717f71 100644 --- a/tests/system/Database/Live/FromTest.php +++ b/tests/system/Database/Live/FromTest.php @@ -24,8 +24,7 @@ final class FromTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testFromCanAddTables() { diff --git a/tests/system/Database/Live/GetNumRowsTest.php b/tests/system/Database/Live/GetNumRowsTest.php index bfa982d5d6a5..791fef5acf39 100644 --- a/tests/system/Database/Live/GetNumRowsTest.php +++ b/tests/system/Database/Live/GetNumRowsTest.php @@ -22,8 +22,7 @@ final class GetNumRowsTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; /** * Added as instructed at https://codeigniter4.github.io/userguide/testing/database.html#the-test-class diff --git a/tests/system/Database/Live/GetTest.php b/tests/system/Database/Live/GetTest.php index a2f1aa4f9b89..2682f12e57ca 100644 --- a/tests/system/Database/Live/GetTest.php +++ b/tests/system/Database/Live/GetTest.php @@ -25,8 +25,7 @@ final class GetTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testGet() { diff --git a/tests/system/Database/Live/GroupTest.php b/tests/system/Database/Live/GroupTest.php index 9ef9da758fff..705172ac97f0 100644 --- a/tests/system/Database/Live/GroupTest.php +++ b/tests/system/Database/Live/GroupTest.php @@ -24,8 +24,7 @@ final class GroupTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testGroupBy() { diff --git a/tests/system/Database/Live/IncrementTest.php b/tests/system/Database/Live/IncrementTest.php index d53732464f56..bb5faccf4e15 100644 --- a/tests/system/Database/Live/IncrementTest.php +++ b/tests/system/Database/Live/IncrementTest.php @@ -24,8 +24,7 @@ final class IncrementTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testIncrement() { diff --git a/tests/system/Database/Live/InsertTest.php b/tests/system/Database/Live/InsertTest.php index 8ca40e2309ef..a5bd090b7578 100644 --- a/tests/system/Database/Live/InsertTest.php +++ b/tests/system/Database/Live/InsertTest.php @@ -24,8 +24,7 @@ final class InsertTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testInsert() { diff --git a/tests/system/Database/Live/JoinTest.php b/tests/system/Database/Live/JoinTest.php index 7e35effcc852..b30c8ac8b9bb 100644 --- a/tests/system/Database/Live/JoinTest.php +++ b/tests/system/Database/Live/JoinTest.php @@ -24,8 +24,7 @@ final class JoinTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testSimpleJoin() { diff --git a/tests/system/Database/Live/LikeTest.php b/tests/system/Database/Live/LikeTest.php index 999539e488fc..65261f2372fb 100644 --- a/tests/system/Database/Live/LikeTest.php +++ b/tests/system/Database/Live/LikeTest.php @@ -24,8 +24,7 @@ final class LikeTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testLikeDefault() { diff --git a/tests/system/Database/Live/LimitTest.php b/tests/system/Database/Live/LimitTest.php index ee3f8c475b79..8673b5bc486d 100644 --- a/tests/system/Database/Live/LimitTest.php +++ b/tests/system/Database/Live/LimitTest.php @@ -22,9 +22,9 @@ final class LimitTest extends CIUnitTestCase { use DatabaseTestTrait; - protected $refresh = true; - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $refresh = true; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testLimit() { diff --git a/tests/system/Database/Live/OrderTest.php b/tests/system/Database/Live/OrderTest.php index 9d87089b5e79..76f7e8374f32 100644 --- a/tests/system/Database/Live/OrderTest.php +++ b/tests/system/Database/Live/OrderTest.php @@ -24,8 +24,7 @@ final class OrderTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testOrderAscending() { diff --git a/tests/system/Database/Live/SelectTest.php b/tests/system/Database/Live/SelectTest.php index 6224633847d6..fbfa3ca97052 100644 --- a/tests/system/Database/Live/SelectTest.php +++ b/tests/system/Database/Live/SelectTest.php @@ -24,8 +24,7 @@ final class SelectTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testSelectAllByDefault() { diff --git a/tests/system/Database/Live/UpdateTest.php b/tests/system/Database/Live/UpdateTest.php index c0f1bd3575d4..f7e2fb5e4b32 100644 --- a/tests/system/Database/Live/UpdateTest.php +++ b/tests/system/Database/Live/UpdateTest.php @@ -25,8 +25,7 @@ final class UpdateTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testUpdateSetsAllWithoutWhere() { diff --git a/tests/system/Database/Live/WhereTest.php b/tests/system/Database/Live/WhereTest.php index ce87bbd3c7f4..76756e19a219 100644 --- a/tests/system/Database/Live/WhereTest.php +++ b/tests/system/Database/Live/WhereTest.php @@ -24,8 +24,7 @@ final class WhereTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testWhereSimpleKeyValue() { diff --git a/tests/system/Database/Live/WriteTypeQueryTest.php b/tests/system/Database/Live/WriteTypeQueryTest.php index 31dd848c41a2..9ec6c1c1dbe3 100644 --- a/tests/system/Database/Live/WriteTypeQueryTest.php +++ b/tests/system/Database/Live/WriteTypeQueryTest.php @@ -25,8 +25,7 @@ final class WriteTypeQueryTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; public function testSet() { diff --git a/tests/system/Database/Migrations/MigrationRunnerTest.php b/tests/system/Database/Migrations/MigrationRunnerTest.php index fa867c7e1f0e..27e14c51c0f7 100644 --- a/tests/system/Database/Migrations/MigrationRunnerTest.php +++ b/tests/system/Database/Migrations/MigrationRunnerTest.php @@ -33,7 +33,6 @@ final class MigrationRunnerTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; - protected $root; protected $start; protected $config; diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index c995f5d7c191..94d71849c679 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -930,14 +930,12 @@ protected function getEntity() 'default' => 'sumfin', 'created_at' => null, ]; - protected $original = [ 'foo' => null, 'bar' => null, 'default' => 'sumfin', 'created_at' => null, ]; - protected $datamap = [ 'createdAt' => 'created_at', ]; @@ -968,7 +966,6 @@ protected function getMappedEntity() 'foo' => null, 'simple' => null, ]; - protected $_original = [ 'foo' => null, 'simple' => null, @@ -999,12 +996,10 @@ protected function getSwappedEntity() 'foo' => 'foo', 'bar' => 'bar', ]; - protected $_original = [ 'foo' => 'foo', 'bar' => 'bar', ]; - protected $datamap = [ 'bar' => 'foo', 'foo' => 'bar', @@ -1031,7 +1026,6 @@ protected function getCastEntity($data = null): Entity 'twelfth' => null, 'thirteenth' => null, ]; - protected $_original = [ 'first' => null, 'second' => null, @@ -1082,7 +1076,6 @@ protected function getCastNullableEntity() 'integer_0' => null, 'string_value_not_null' => 'value', ]; - protected $_original = [ 'string_null' => null, 'string_empty' => null, @@ -1111,7 +1104,6 @@ protected function getCustomCastEntity() 'third' => null, 'fourth' => null, ]; - protected $_original = [ 'first' => null, 'second' => null, @@ -1126,7 +1118,6 @@ protected function getCustomCastEntity() 'third' => 'type[param1, param2,param3]', 'fourth' => '?type', ]; - protected $castHandlers = [ 'base64' => CastBase64::class, 'someType' => NotExtendsBaseCast::class, diff --git a/tests/system/HTTP/RedirectResponseTest.php b/tests/system/HTTP/RedirectResponseTest.php index 0e8e49a74588..ac2e8abaaba9 100644 --- a/tests/system/HTTP/RedirectResponseTest.php +++ b/tests/system/HTTP/RedirectResponseTest.php @@ -30,6 +30,7 @@ final class RedirectResponseTest extends CIUnitTestCase * @var RouteCollection */ protected $routes; + protected $request; protected $config; diff --git a/tests/system/Models/InsertModelTest.php b/tests/system/Models/InsertModelTest.php index 38b86eb67e30..521dec8fbadd 100644 --- a/tests/system/Models/InsertModelTest.php +++ b/tests/system/Models/InsertModelTest.php @@ -165,7 +165,6 @@ public function testInsertBatchNewEntityWithDateTime(): void protected $deleted; protected $created_at; protected $updated_at; - protected $_options = [ 'datamap' => [], 'dates' => [ @@ -223,7 +222,6 @@ public function testInsertEntityWithNoDataExceptionNoAllowedData(): void protected $deleted; protected $created_at; protected $updated_at; - protected $_options = [ 'datamap' => [], 'dates' => [ diff --git a/tests/system/Models/SaveModelTest.php b/tests/system/Models/SaveModelTest.php index 17f56a4cb88c..48b63c14b211 100644 --- a/tests/system/Models/SaveModelTest.php +++ b/tests/system/Models/SaveModelTest.php @@ -216,7 +216,6 @@ public function testSaveNewEntityWithDateTime(): void protected $deleted; protected $created_at; protected $updated_at; - protected $_options = [ 'datamap' => [], 'dates' => [ diff --git a/tests/system/Models/UpdateModelTest.php b/tests/system/Models/UpdateModelTest.php index 619028a748aa..888c019eca1e 100644 --- a/tests/system/Models/UpdateModelTest.php +++ b/tests/system/Models/UpdateModelTest.php @@ -166,7 +166,6 @@ public function testUpdateBatchWithEntity(): void protected $deleted; protected $created_at; protected $updated_at; - protected $_options = [ 'datamap' => [], 'dates' => [ @@ -186,7 +185,6 @@ public function testUpdateBatchWithEntity(): void protected $deleted; protected $created_at; protected $updated_at; - protected $_options = [ 'datamap' => [], 'dates' => [ @@ -315,7 +313,6 @@ public function testUpdateWithEntityNoAllowedFields(): void protected $deleted; protected $created_at; protected $updated_at; - protected $_options = [ 'datamap' => [], 'dates' => [ diff --git a/tests/system/Pager/PagerTest.php b/tests/system/Pager/PagerTest.php index 72deb09c2c4c..05b069d83c21 100644 --- a/tests/system/Pager/PagerTest.php +++ b/tests/system/Pager/PagerTest.php @@ -29,6 +29,7 @@ final class PagerTest extends CIUnitTestCase * @var \CodeIgniter\Pager\Pager */ protected $pager; + protected $config; protected function setUp(): void diff --git a/tests/system/Session/Handlers/DatabaseHandlerTest.php b/tests/system/Session/Handlers/DatabaseHandlerTest.php index e001af8eeec8..2349bc292bd8 100644 --- a/tests/system/Session/Handlers/DatabaseHandlerTest.php +++ b/tests/system/Session/Handlers/DatabaseHandlerTest.php @@ -26,8 +26,7 @@ final class DatabaseHandlerTest extends CIUnitTestCase use ReflectionHelper; protected $refresh = true; - - protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; protected function setUp(): void { diff --git a/tests/system/Validation/CreditCardRulesTest.php b/tests/system/Validation/CreditCardRulesTest.php index a1e8f74d8a27..7610ea4ee966 100644 --- a/tests/system/Validation/CreditCardRulesTest.php +++ b/tests/system/Validation/CreditCardRulesTest.php @@ -24,6 +24,7 @@ final class CreditCardRulesTest extends CIUnitTestCase * @var Validation */ protected $validation; + protected $config = [ 'ruleSets' => [ Rules::class, diff --git a/tests/system/Validation/FileRulesTest.php b/tests/system/Validation/FileRulesTest.php index 0e4e70ec537f..e8da364367a7 100644 --- a/tests/system/Validation/FileRulesTest.php +++ b/tests/system/Validation/FileRulesTest.php @@ -24,6 +24,7 @@ final class FileRulesTest extends CIUnitTestCase * @var Validation */ protected $validation; + protected $config = [ 'ruleSets' => [ Rules::class, diff --git a/tests/system/Validation/FormatRulesTest.php b/tests/system/Validation/FormatRulesTest.php index 725824607748..eb457a0c3a2f 100644 --- a/tests/system/Validation/FormatRulesTest.php +++ b/tests/system/Validation/FormatRulesTest.php @@ -27,6 +27,7 @@ final class FormatRulesTest extends CIUnitTestCase * @var Validation */ protected $validation; + protected $config = [ 'ruleSets' => [ Rules::class, diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index f4f171b87bbb..7fe8469fcbeb 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -31,6 +31,7 @@ final class RulesTest extends CIUnitTestCase * @var Validation */ protected $validation; + protected $config = [ 'ruleSets' => [ Rules::class, diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index ccbff3358f42..8b77686fc3b5 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -29,6 +29,7 @@ final class ValidationTest extends CIUnitTestCase * @var Validation */ protected $validation; + protected $config = [ 'ruleSets' => [ Rules::class, diff --git a/tests/system/View/ParserPluginTest.php b/tests/system/View/ParserPluginTest.php index df773a375df8..85de3e7898fa 100644 --- a/tests/system/View/ParserPluginTest.php +++ b/tests/system/View/ParserPluginTest.php @@ -24,6 +24,7 @@ final class ParserPluginTest extends CIUnitTestCase * @var Parser */ protected $parser; + /** * @var Validation */ From abe42861f6c4267f7b72082cac59a3a8023a16f1 Mon Sep 17 00:00:00 2001 From: Tetsuro Yoshikawa Date: Mon, 11 Oct 2021 22:15:09 +0900 Subject: [PATCH 291/490] fix: fieldName -> keyName --- system/Database/Forge.php | 6 +++--- system/Database/MySQLi/Forge.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/system/Database/Forge.php b/system/Database/Forge.php index c4bd082b73c4..8e0b7e43c7f3 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -433,13 +433,13 @@ public function addForeignKey($fieldName = '', string $tableName = '', $tableFie * * @throws DatabaseException * - * @return BaseResult|bool|false|mixed|Query + * @return bool */ - public function dropKey(string $table, string $fieldName) + public function dropKey(string $table, string $keyName) { $sql = sprintf( $this->dropIndexStr, - $this->db->escapeIdentifiers($this->db->DBPrefix . $fieldName), + $this->db->escapeIdentifiers($this->db->DBPrefix . $keyName), $this->db->escapeIdentifiers($this->db->DBPrefix . $table), ); diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php index 3038f8a9165f..d00c26dd1ca7 100644 --- a/system/Database/MySQLi/Forge.php +++ b/system/Database/MySQLi/Forge.php @@ -224,13 +224,13 @@ protected function _processIndexes(string $table): string /** * Drop Key * - * @return BaseResult|bool|false|mixed|Query + * @return bool */ - public function dropKey(string $table, string $fieldName) + public function dropKey(string $table, string $keyName) { $sql = sprintf( $this->dropIndexStr, - $this->db->escapeIdentifiers($fieldName), + $this->db->escapeIdentifiers($keyName), $this->db->escapeIdentifiers($this->db->DBPrefix . $table), ); From eda426ddd9df34bb9ed3b49f3c534d446af65674 Mon Sep 17 00:00:00 2001 From: Tetsuro Yoshikawa Date: Mon, 11 Oct 2021 22:15:27 +0900 Subject: [PATCH 292/490] docs: remove unnecessary escape. --- user_guide_src/source/dbmgmt/forge.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/dbmgmt/forge.rst b/user_guide_src/source/dbmgmt/forge.rst index d9b3bd631dbf..d7b3c4546825 100644 --- a/user_guide_src/source/dbmgmt/forge.rst +++ b/user_guide_src/source/dbmgmt/forge.rst @@ -312,7 +312,7 @@ Execute a DROP KEY. :: - // Produces: DROP INDEX 'users_index' ON 'tablename' + // Produces: DROP INDEX users_index ON tablename $forge->dropKey('tablename','users_index'); Renaming a table From 2126376e8998732fc7341517fe9e11ea39b788c0 Mon Sep 17 00:00:00 2001 From: Tetsuro Yoshikawa Date: Mon, 11 Oct 2021 22:27:25 +0900 Subject: [PATCH 293/490] docs: remove single-quote escaping. --- user_guide_src/source/dbmgmt/forge.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/dbmgmt/forge.rst b/user_guide_src/source/dbmgmt/forge.rst index 6cbdfe506e8a..a0c677d3162b 100644 --- a/user_guide_src/source/dbmgmt/forge.rst +++ b/user_guide_src/source/dbmgmt/forge.rst @@ -301,7 +301,7 @@ Execute a DROP FOREIGN KEY. :: - // Produces: ALTER TABLE 'tablename' DROP FOREIGN KEY 'users_foreign' + // Produces: ALTER TABLE tablename DROP FOREIGN KEY users_foreign $forge->dropForeignKey('tablename','users_foreign'); Renaming a table From 1d091fab3bdd259e9ace6327db0b2d03a90fcd28 Mon Sep 17 00:00:00 2001 From: Alex Schmitz Date: Tue, 12 Oct 2021 00:43:31 +0200 Subject: [PATCH 294/490] Add formatted query string to timeline. Co-Authored-By: Daniel Tiringer <53534182+danielTiringer@users.noreply.github.com> --- system/Debug/Toolbar.php | 11 +++++++++++ system/Debug/Toolbar/Collectors/Database.php | 1 + 2 files changed, 12 insertions(+) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index b59666aacc4c..2f2e7d0d92b4 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -273,6 +273,17 @@ protected function structureTimelineData(array $elements): array // We define ourselves as the first element of the array $element = array_shift($elements); + // If we are a query, add the formatted query string as our first child + if (array_key_exists('query', $element) && $element['query']) { + $element['children'][] = [ + 'name' => $element['query'], + 'component' => 'Query', + 'start' => $element['start'], + 'duration' => $element['duration'], + 'end' => $element['end'], + ]; + } + // If we have children behind us, collect and attach them to us while (! empty($elements) && $elements[array_key_first($elements)]['end'] <= $element['end']) { $element['children'][] = array_shift($elements); diff --git a/system/Debug/Toolbar/Collectors/Database.php b/system/Debug/Toolbar/Collectors/Database.php index c093068a9342..d8445b80364f 100644 --- a/system/Debug/Toolbar/Collectors/Database.php +++ b/system/Debug/Toolbar/Collectors/Database.php @@ -118,6 +118,7 @@ protected function formatTimelineData(): array 'component' => 'Database', 'start' => $query['query']->getStartTime(true), 'duration' => $query['query']->getDuration(), + 'query' => $query['query']->debugToolbarDisplay(), ]; } From d02c38071f45d4d891624b7a4e9d931a7eaf4ab9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 09:17:08 +0900 Subject: [PATCH 295/490] docs: move contributing related contents from README.md to contributing/README.md --- README.md | 8 +------- contributing/README.md | 6 ++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5d9d007b91e2..1bcfdfcb922d 100644 --- a/README.md +++ b/README.md @@ -71,13 +71,7 @@ to optional packages, with their own repository. We **are** accepting contributions from the community! -We will try to manage the process somewhat, by adding a ["help wanted" label](https://github.com/codeigniter4/CodeIgniter4/labels/help%20wanted) to those that we are -specifically interested in at any point in time. Join the discussion for those issues and let us know -if you want to take the lead on one of them. - -At this time, we are not looking for out-of-scope contributions, only those that would be considered part of our controlled evolution! - -Please read the [*Contributing to CodeIgniter*](https://github.com/codeigniter4/CodeIgniter4/blob/develop/CONTRIBUTING.md) section in the user guide. +Please read the [*Contributing to CodeIgniter*](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/README.md). ## Server Requirements diff --git a/contributing/README.md b/contributing/README.md index 2df6e0870ebc..7277eb1dc25d 100644 --- a/contributing/README.md +++ b/contributing/README.md @@ -7,6 +7,12 @@ Requests](https://help.github.com/articles/using-pull-requests/) on the [CodeIgniter4 repository](https://github.com/codeigniter4/CodeIgniter4) on GitHub. +We will try to manage the process somewhat, by adding a ["help wanted" label](https://github.com/codeigniter4/CodeIgniter4/labels/help%20wanted) to those that we are +specifically interested in at any point in time. Join the discussion for those issues and let us know +if you want to take the lead on one of them. + +At this time, we are not looking for out-of-scope contributions, only those that would be considered part of our controlled evolution! + - [Contributor Covenant Code of Conduct](../CODE_OF_CONDUCT.md) - [Reporting a Bug](./bug_report.md) - [Sending a Pull Request](./pull_request.md) From 99a1a53905f7fc6b599fb6b6ee6f8e94cc8f9d0b Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 09:18:54 +0900 Subject: [PATCH 296/490] docs: remove "we are not looking for out-of-scope contributions" It seems it is out-of-dated. --- contributing/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/contributing/README.md b/contributing/README.md index 7277eb1dc25d..40796df376b3 100644 --- a/contributing/README.md +++ b/contributing/README.md @@ -11,8 +11,6 @@ We will try to manage the process somewhat, by adding a ["help wanted" label](ht specifically interested in at any point in time. Join the discussion for those issues and let us know if you want to take the lead on one of them. -At this time, we are not looking for out-of-scope contributions, only those that would be considered part of our controlled evolution! - - [Contributor Covenant Code of Conduct](../CODE_OF_CONDUCT.md) - [Reporting a Bug](./bug_report.md) - [Sending a Pull Request](./pull_request.md) From c1f72de2af3a2bfeb5fdd9fcc815cfdb2866cb1b Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 12:46:22 +0900 Subject: [PATCH 297/490] docs: remove unnecessary indentation --- user_guide_src/source/helpers/form_helper.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/helpers/form_helper.rst b/user_guide_src/source/helpers/form_helper.rst index 685c7e7041ea..be36f9c9dd09 100644 --- a/user_guide_src/source/helpers/form_helper.rst +++ b/user_guide_src/source/helpers/form_helper.rst @@ -103,7 +103,7 @@ The following functions are available: will return::
    - + **Adding Hidden Input Fields** From 205cdfb2e707863138af0248446aa55d455ca3f1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 12:47:56 +0900 Subject: [PATCH 298/490] docs: add note to CSRF filter and HTTP method --- user_guide_src/source/helpers/form_helper.rst | 2 ++ user_guide_src/source/libraries/security.rst | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/helpers/form_helper.rst b/user_guide_src/source/helpers/form_helper.rst index be36f9c9dd09..f4e5b09c1af1 100644 --- a/user_guide_src/source/helpers/form_helper.rst +++ b/user_guide_src/source/helpers/form_helper.rst @@ -105,6 +105,8 @@ The following functions are available: + .. note:: To use auto generation of CSRF field, you need to turn CSRF filter on to the form page. In most cases it is requested using the `GET` method. + **Adding Hidden Input Fields** Hidden fields can be added by passing an associative array to the diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index 0a16f99e022a..2d16a2a5b53a 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -53,7 +53,12 @@ Regular expressions are also supported (case-insensitive):: If you use the :doc:`form helper <../helpers/form_helper>`, then :func:`form_open()` will automatically insert a hidden csrf field in -your forms. If not, then you can use the always available ``csrf_token()`` +your forms. + +.. note:: To use auto generation of CSRF field, you need to turn CSRF filter on to the form page. + In most cases it is requested using the `GET` method. + +If not, then you can use the always available ``csrf_token()`` and ``csrf_hash()`` functions :: From f562184c051fa7becc822b3995a2a9e76dbe3035 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 12:48:43 +0900 Subject: [PATCH 299/490] docs: fix typo --- user_guide_src/source/installation/upgrade_security.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_security.rst b/user_guide_src/source/installation/upgrade_security.rst index 4f2b8107a96b..61cf699658b2 100644 --- a/user_guide_src/source/installation/upgrade_security.rst +++ b/user_guide_src/source/installation/upgrade_security.rst @@ -30,7 +30,7 @@ Upgrade Guide ] ]; -2. Within you html forms you have to remove the csrf input which looks similar to ````. +2. Within your html forms you have to remove the csrf input which looks similar to ````. 3. Now within your html forms you have to add ```` somewhere in the form body, unless you are using ``form_open()``. Code Example From 33f4635244541a1b285a1bd93aad340e2fec3d97 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 12:49:35 +0900 Subject: [PATCH 300/490] docs: add headings There are a lot of contents. --- user_guide_src/source/libraries/security.rst | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index 2d16a2a5b53a..758d16b54173 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -23,6 +23,9 @@ If you find a case where you do need direct access though, you may load it throu Cross-site request forgery (CSRF) ********************************* +Enable CSRF Protection +====================== + You can enable CSRF protection by altering your **app/Config/Filters.php** and enabling the `csrf` filter globally:: @@ -51,6 +54,9 @@ Regular expressions are also supported (case-insensitive):: ], ]; +HTML Forms +========== + If you use the :doc:`form helper <../helpers/form_helper>`, then :func:`form_open()` will automatically insert a hidden csrf field in your forms. @@ -80,12 +86,18 @@ meta tag for you:: // Generates: +The Order of Token Sent by Users +================================ + The order of checking the availability of the CSRF token is as follows: 1. ``$_POST`` array -2. Http header +2. HTTP header 3. ``php://input`` (JSON request) - bare in mind that this approach is the slowest one since we have to decode JSON and then encode it again +Tokens Regeneration +=================== + Tokens may be either regenerated on every submission (default) or kept the same throughout the life of the CSRF cookie. The default regeneration of tokens provides stricter security, but may result @@ -96,6 +108,9 @@ may alter this behavior by editing the following config parameter value in public $regenerate = true; +Redirection on Failure +====================== + When a request fails the CSRF validation check, it will redirect to the previous page by default, setting an ``error`` flash message that you can display to the end user. This provides a nicer experience than simply crashing. This can be turned off by editing the following config parameter value in From 69fbed9a5969a80bb17ffecf36272e19dd0496dd Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 13:39:31 +0900 Subject: [PATCH 301/490] docs: remove unnecessary blank lines --- admin/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/admin/README.md b/admin/README.md index 52747ca3eeaa..9f36d2d72a48 100644 --- a/admin/README.md +++ b/admin/README.md @@ -9,7 +9,6 @@ This folder contains tools or docs useful for project maintainers. In addition to the framework source, it includes unit testing and documentation source. The three repositories following are built from this one as part of the release workflow. This repo is meant to be forked by contributors. - - **framework** is the released developer repository. It contains all the main pieces of the framework that developers would use to build their apps, but not the framework unit testing or the user guide source. @@ -25,7 +24,6 @@ This folder contains tools or docs useful for project maintainers. framework releases. It could be downloaded, forked or potentially composer-installed. This is a read-only repository. - - **coding-standard** is the coding style standards repository. It contains PHP CodeSniffer rules to ensure consistent code style within the framework itself. From aa81929094ebf4210154ebc2e1ed3f993ccd9a44 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 13:39:57 +0900 Subject: [PATCH 302/490] docs: update about coding-standard repository --- admin/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/README.md b/admin/README.md index 9f36d2d72a48..1f5dd89bfcd1 100644 --- a/admin/README.md +++ b/admin/README.md @@ -24,8 +24,8 @@ This folder contains tools or docs useful for project maintainers. framework releases. It could be downloaded, forked or potentially composer-installed. This is a read-only repository. -- **coding-standard** is the coding style standards repository. - It contains PHP CodeSniffer rules to ensure consistent code style +- **coding-standard** is the coding style standards repository. + It contains PHP-CS-Fixer rules to ensure consistent code style within the framework itself. It is meant to be composer-installed. - **translations** is the repository holding official translations of From cc0b7e996951d32b53874d81be8b90b6eb05e064 Mon Sep 17 00:00:00 2001 From: Alex Schmitz Date: Tue, 12 Oct 2021 09:41:44 +0200 Subject: [PATCH 303/490] Simplify query string output in timeline. --- system/Debug/Toolbar.php | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 2f2e7d0d92b4..491be10e5af6 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -185,16 +185,18 @@ protected function renderTimelineRecursive(array $rows, float $startTime, int $s foreach ($rows as $row) { $hasChildren = isset($row['children']) && ! empty($row['children']); + $isQuery = isset($row['query']) && ! empty($row['query']); + // Open controller timeline by default $open = $row['name'] === 'Controller'; - if ($hasChildren) { + if ($hasChildren || $isQuery) { $output .= '
    '; } else { $output .= ''; } - $output .= ''; + $output .= ''; $output .= ''; $output .= ''; $output .= "'; $output .= ''; @@ -273,17 +285,6 @@ protected function structureTimelineData(array $elements): array // We define ourselves as the first element of the array $element = array_shift($elements); - // If we are a query, add the formatted query string as our first child - if (array_key_exists('query', $element) && $element['query']) { - $element['children'][] = [ - 'name' => $element['query'], - 'component' => 'Query', - 'start' => $element['start'], - 'duration' => $element['duration'], - 'end' => $element['end'], - ]; - } - // If we have children behind us, collect and attach them to us while (! empty($elements) && $elements[array_key_first($elements)]['end'] <= $element['end']) { $element['children'][] = array_shift($elements); From 8d2678f0cd47789eac4162b052882c5deb1bfb08 Mon Sep 17 00:00:00 2001 From: Alex Schmitz <40514119+sfadschm@users.noreply.github.com> Date: Wed, 13 Oct 2021 07:25:43 +0200 Subject: [PATCH 304/490] Remove duplicate parentheses. Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com> --- system/Debug/Toolbar.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 491be10e5af6..2199559d362d 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -196,7 +196,7 @@ protected function renderTimelineRecursive(array $rows, float $startTime, int $s $output .= ''; } - $output .= ''; + $output .= ''; $output .= ''; $output .= ''; $output .= "
    {duration} {! sql !}
    ' . ($hasChildren ? '' : '') . $row['name'] . '' . (($hasChildren || $isQuery) ? '' : '') . $row['name'] . '' . $row['component'] . '' . number_format($row['duration'] * 1000, 2) . ' ms"; @@ -211,12 +213,22 @@ protected function renderTimelineRecursive(array $rows, float $startTime, int $s $styleCount++; // Add children if any - if ($hasChildren) { + if ($hasChildren || $isQuery) { $output .= '
    '; $output .= ''; $output .= ''; - $output .= $this->renderTimelineRecursive($row['children'], $startTime, $segmentCount, $segmentDuration, $styles, $styleCount, $level + 1, true); + + if ($isQuery) { + // Output query string if query + $output .= ''; + $output .= ''; + $output .= ''; + } else { + // Recursively render children + $output .= $this->renderTimelineRecursive($row['children'], $startTime, $segmentCount, $segmentDuration, $styles, $styleCount, $level + 1, true); + } + $output .= ''; $output .= '
    ' . $row['query'] . '
    '; $output .= '
    ' . (($hasChildren || $isQuery) ? '' : '') . $row['name'] . '' . ($hasChildren || $isQuery ? '' : '') . $row['name'] . '' . $row['component'] . '' . number_format($row['duration'] * 1000, 2) . ' ms"; From e393aac29dec267f11a454a2ef93f226f35f0db2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 13 Oct 2021 14:30:16 +0900 Subject: [PATCH 305/490] docs: fix by proofreading Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com> --- user_guide_src/source/helpers/form_helper.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/helpers/form_helper.rst b/user_guide_src/source/helpers/form_helper.rst index f4e5b09c1af1..e8788de0a499 100644 --- a/user_guide_src/source/helpers/form_helper.rst +++ b/user_guide_src/source/helpers/form_helper.rst @@ -105,7 +105,7 @@ The following functions are available: - .. note:: To use auto generation of CSRF field, you need to turn CSRF filter on to the form page. In most cases it is requested using the `GET` method. + .. note:: To use auto-generation of CSRF field, you need to turn CSRF filter on to the form page. In most cases it is requested using the ``GET`` method. **Adding Hidden Input Fields** From 105253c076a2da292d5bda59b69b36635a6ee018 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 13 Oct 2021 14:31:42 +0900 Subject: [PATCH 306/490] docs: fix by proofreading Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com> --- user_guide_src/source/installation/upgrade_security.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_security.rst b/user_guide_src/source/installation/upgrade_security.rst index 61cf699658b2..5f2d8ecfdb66 100644 --- a/user_guide_src/source/installation/upgrade_security.rst +++ b/user_guide_src/source/installation/upgrade_security.rst @@ -30,8 +30,8 @@ Upgrade Guide ] ]; -2. Within your html forms you have to remove the csrf input which looks similar to ````. -3. Now within your html forms you have to add ```` somewhere in the form body, unless you are using ``form_open()``. +2. Within your HTML forms you have to remove the CSRF input field which looks similar to ````. +3. Now, within your HTML forms you have to add ```` somewhere in the form body, unless you are using ``form_open()``. Code Example ============ From a790f87b1d6c2c38b904a18c130124da0d3342f2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 13 Oct 2021 14:31:50 +0900 Subject: [PATCH 307/490] docs: fix by proofreading Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com> --- user_guide_src/source/libraries/security.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index 758d16b54173..3e173aa4bc45 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -61,8 +61,8 @@ If you use the :doc:`form helper <../helpers/form_helper>`, then :func:`form_open()` will automatically insert a hidden csrf field in your forms. -.. note:: To use auto generation of CSRF field, you need to turn CSRF filter on to the form page. - In most cases it is requested using the `GET` method. +.. note:: To use auto-generation of CSRF field, you need to turn CSRF filter on to the form page. + In most cases it is requested using the ``GET`` method. If not, then you can use the always available ``csrf_token()`` and ``csrf_hash()`` functions From b3cda21155714546e64da3045003a95a57d96226 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 13 Oct 2021 14:34:12 +0900 Subject: [PATCH 308/490] docs: fix by proofreading Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com> --- user_guide_src/source/libraries/security.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index 3e173aa4bc45..3c2ad833f1d8 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -93,7 +93,7 @@ The order of checking the availability of the CSRF token is as follows: 1. ``$_POST`` array 2. HTTP header -3. ``php://input`` (JSON request) - bare in mind that this approach is the slowest one since we have to decode JSON and then encode it again +3. ``php://input`` (JSON request) - bear in mind that this approach is the slowest one since we have to decode JSON and then re-encode it Tokens Regeneration =================== From 51c070fe496e891763cfc1025f8395ecc09a0c75 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 13 Oct 2021 14:34:20 +0900 Subject: [PATCH 309/490] docs: fix by proofreading Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com> --- user_guide_src/source/libraries/security.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index 3c2ad833f1d8..9497723c4b46 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -95,7 +95,7 @@ The order of checking the availability of the CSRF token is as follows: 2. HTTP header 3. ``php://input`` (JSON request) - bear in mind that this approach is the slowest one since we have to decode JSON and then re-encode it -Tokens Regeneration +Token Regeneration =================== Tokens may be either regenerated on every submission (default) or From 125052ade293f46a30b7cc4d5363caf7709c1d9e Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 14 Oct 2021 02:11:05 +0900 Subject: [PATCH 310/490] docs: Github -> GitHub --- README.md | 4 ++-- admin/css/debug-toolbar/_settings.scss | 2 +- admin/framework/README.md | 2 +- admin/starter/README.md | 2 +- tests/_support/Config/Registrar.php | 2 +- user_guide_src/ghpages.rst | 2 +- user_guide_src/source/installation/repositories.rst | 6 +++--- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1bcfdfcb922d..fa5194457cbc 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ CodeIgniter is developed completely on a volunteer basis. As such, please give u for your issues to be reviewed. If you haven't heard from one of the team in that time period, feel free to leave a comment on the issue so that it gets brought back to our attention. -We use Github issues to track **BUGS** and to track approved **DEVELOPMENT** work packages. +We use GitHub issues to track **BUGS** and to track approved **DEVELOPMENT** work packages. We use our [forum](http://forum.codeigniter.com) to provide SUPPORT and to discuss FEATURE REQUESTS. @@ -54,7 +54,7 @@ If you raise an issue here that pertains to support or a feature request, it wil be closed! If you are not sure if you have found a bug, raise a thread on the forum first - someone else may have encountered the same thing. -Before raising a new Github issue, please check that your bug hasn't already +Before raising a new GitHub issue, please check that your bug hasn't already been reported or fixed. We use pull requests (PRs) for CONTRIBUTIONS to the repository. diff --git a/admin/css/debug-toolbar/_settings.scss b/admin/css/debug-toolbar/_settings.scss index 06a51de634bb..1bb1386a46a0 100644 --- a/admin/css/debug-toolbar/_settings.scss +++ b/admin/css/debug-toolbar/_settings.scss @@ -1,7 +1,7 @@ // FONT // ========================================================================== */ -// Standard "sans-serif" font stack used by Github +// Standard "sans-serif" font stack used by GitHub $base-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; // Default size, all other styles are based on this size diff --git a/admin/framework/README.md b/admin/framework/README.md index 1f95a415d37a..6b7c6673e33a 100644 --- a/admin/framework/README.md +++ b/admin/framework/README.md @@ -28,7 +28,7 @@ framework are exposed. ## Repository Management -We use Github issues, in our main repository, to track **BUGS** and to track approved **DEVELOPMENT** work packages. +We use GitHub issues, in our main repository, to track **BUGS** and to track approved **DEVELOPMENT** work packages. We use our [forum](http://forum.codeigniter.com) to provide SUPPORT and to discuss FEATURE REQUESTS. diff --git a/admin/starter/README.md b/admin/starter/README.md index 41ce92110d7a..363e7c89f304 100644 --- a/admin/starter/README.md +++ b/admin/starter/README.md @@ -41,7 +41,7 @@ framework are exposed. ## Repository Management -We use Github issues, in our main repository, to track **BUGS** and to track approved **DEVELOPMENT** work packages. +We use GitHub issues, in our main repository, to track **BUGS** and to track approved **DEVELOPMENT** work packages. We use our [forum](http://forum.codeigniter.com) to provide SUPPORT and to discuss FEATURE REQUESTS. diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php index 15e8f54ff92d..989b406655b1 100644 --- a/tests/_support/Config/Registrar.php +++ b/tests/_support/Config/Registrar.php @@ -111,7 +111,7 @@ public static function Database() { $config = []; - // Under Github Actions, we can set an ENV var named 'DB' + // Under GitHub Actions, we can set an ENV var named 'DB' // so that we can test against multiple databases. if ($group = getenv('DB')) { if (! empty(self::$dbConfig[$group])) { diff --git a/user_guide_src/ghpages.rst b/user_guide_src/ghpages.rst index 4699d38f978d..15619a060e0d 100644 --- a/user_guide_src/ghpages.rst +++ b/user_guide_src/ghpages.rst @@ -6,7 +6,7 @@ The intent is, eventually, for the in-progress user guide to be automatically generated as part of a PR merge. This writeup explains how it can be done manually in the meantime. -The user guide takes advantage of Github pages, where the "gh-pages" branch of +The user guide takes advantage of GitHub pages, where the "gh-pages" branch of a repo, containing HTML only, is accessible through `github.io `_. diff --git a/user_guide_src/source/installation/repositories.rst b/user_guide_src/source/installation/repositories.rst index b585ca8e8f6b..9e4e0abc9db9 100644 --- a/user_guide_src/source/installation/repositories.rst +++ b/user_guide_src/source/installation/repositories.rst @@ -2,7 +2,7 @@ CodeIgniter Repositories ######################## The CodeIgniter 4 open source project has its own -`Github organization `_. +`GitHub organization `_. There are several development repositories, of interest to potential contributors: @@ -33,7 +33,7 @@ are not directly contributed to. In all the above, the latest version of a repository can be downloaded by selecting the "releases" link in the secondary navbar inside -the "Code" tab of its Github repository page. The current (in development) version of each can +the "Code" tab of its GitHub repository page. The current (in development) version of each can be cloned or downloaded by selecting the "Clone or download" dropdown button on the right-hand side if the repository homepage. @@ -54,7 +54,7 @@ CodeIgniter 4 Projects ====================== We maintain a `codeigniter4projects `_ organization -on Github as well, with projects that are not part of the framework, +on GitHub as well, with projects that are not part of the framework, but which showcase it or make it easier to work with! +------------------+--------------+-----------------------------------------------------------------+ From 101db1523eedcaa01c7580403b0b321becaac3ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Oct 2021 01:11:54 +0800 Subject: [PATCH 311/490] chore(deps-dev): update rector/rector requirement Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/0.11.57...0.11.58) --- updated-dependencies: - dependency-name: rector/rector dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fd8b7198f11e..8a8fdaf00aa2 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "phpstan/phpstan": "^0.12.91", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1", - "rector/rector": "0.11.57", + "rector/rector": "0.11.58", "symplify/package-builder": "^9.3" }, "suggest": { From 00b6e325f63623b7ea1d21c7dce2f1987f063cd5 Mon Sep 17 00:00:00 2001 From: ytetsuro Date: Fri, 15 Oct 2021 16:23:48 +0900 Subject: [PATCH 312/490] docs: add escape for identifiers. --- user_guide_src/source/dbmgmt/forge.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/dbmgmt/forge.rst b/user_guide_src/source/dbmgmt/forge.rst index d7b3c4546825..66d7ebd75e09 100644 --- a/user_guide_src/source/dbmgmt/forge.rst +++ b/user_guide_src/source/dbmgmt/forge.rst @@ -312,7 +312,7 @@ Execute a DROP KEY. :: - // Produces: DROP INDEX users_index ON tablename + // Produces: DROP INDEX `users_index` ON `tablename` $forge->dropKey('tablename','users_index'); Renaming a table From 22a4c24798b7549ca9bc1c070039fbd03aa6c78d Mon Sep 17 00:00:00 2001 From: ytetsuro Date: Fri, 15 Oct 2021 16:29:55 +0900 Subject: [PATCH 313/490] docs: add escape for identifiers. --- user_guide_src/source/dbmgmt/forge.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/user_guide_src/source/dbmgmt/forge.rst b/user_guide_src/source/dbmgmt/forge.rst index a0c677d3162b..6fedb1608af5 100644 --- a/user_guide_src/source/dbmgmt/forge.rst +++ b/user_guide_src/source/dbmgmt/forge.rst @@ -46,7 +46,7 @@ or will check if a database exists before create it (depending on DBMS). :: $forge->createDatabase('my_db', true); - // gives CREATE DATABASE IF NOT EXISTS my_db + // gives CREATE DATABASE IF NOT EXISTS `my_db` // or will check if a database exists **$forge->dropDatabase('db_name')** @@ -182,7 +182,7 @@ Primary Key. :: $forge->addField('id'); - // gives id INT(9) NOT NULL AUTO_INCREMENT + // gives `id` INT(9) NOT NULL AUTO_INCREMENT Adding Keys =========== @@ -280,10 +280,10 @@ Execute a DROP TABLE statement and optionally add an IF EXISTS clause. :: - // Produces: DROP TABLE table_name + // Produces: DROP TABLE `table_name` $forge->dropTable('table_name'); - // Produces: DROP TABLE IF EXISTS table_name + // Produces: DROP TABLE IF EXISTS `table_name` $forge->dropTable('table_name', true); A third parameter can be passed to add a "CASCADE" option, which might be required for some @@ -291,7 +291,7 @@ drivers to handle removal of tables with foreign keys. :: - // Produces: DROP TABLE table_name CASCADE + // Produces: DROP TABLE `table_name` CASCADE $forge->dropTable('table_name', false, true); Dropping a Foreign Key @@ -301,7 +301,7 @@ Execute a DROP FOREIGN KEY. :: - // Produces: ALTER TABLE tablename DROP FOREIGN KEY users_foreign + // Produces: ALTER TABLE `tablename` DROP FOREIGN KEY `users_foreign` $forge->dropForeignKey('tablename','users_foreign'); Renaming a table @@ -312,7 +312,7 @@ Executes a TABLE rename :: $forge->renameTable('old_table_name', 'new_table_name'); - // gives ALTER TABLE old_table_name RENAME TO new_table_name + // gives ALTER TABLE `old_table_name` RENAME TO `new_table_name` **************** Modifying Tables @@ -333,7 +333,7 @@ number of additional fields. 'preferences' => ['type' => 'TEXT'] ]; $forge->addColumn('table_name', $fields); - // Executes: ALTER TABLE table_name ADD preferences TEXT + // Executes: ALTER TABLE `table_name` ADD `preferences` TEXT If you are using MySQL or CUBIRD, then you can take advantage of their AFTER and FIRST clauses to position the new column. @@ -386,7 +386,7 @@ change the name, you can add a "name" key into the field defining array. ], ]; $forge->modifyColumn('table_name', $fields); - // gives ALTER TABLE table_name CHANGE old_name new_name TEXT + // gives ALTER TABLE `table_name` CHANGE `old_name` `new_name` TEXT *************** Class Reference From 2007d6434d9fc1d5cf30271c82a4459e92b403e2 Mon Sep 17 00:00:00 2001 From: Alex Schmitz <40514119+sfadschm@users.noreply.github.com> Date: Sat, 16 Oct 2021 17:04:50 +0200 Subject: [PATCH 314/490] [Debug] Improve keyword highlighting and escaping of query strings * Sort highlighted keywords alphabetically. * Add new composite keywords. * Deconstruct composite keywords. * Add basic test for query highlighting in toolbar. * Ignore keywords in string values of queries. * Use generic table and column names for tests. * Add credits for regex pattern. * Escape HTML entities when formatting query string. * Use esc() for escaping query string. --- system/Database/Query.php | 53 ++++++++++++++----------- tests/system/Database/BaseQueryTest.php | 33 +++++++++++++++ 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/system/Database/Query.php b/system/Database/Query.php index 01d27d8bdfff..a4bb1b2b9de5 100644 --- a/system/Database/Query.php +++ b/system/Database/Query.php @@ -367,46 +367,53 @@ public function debugToolbarDisplay(): string { // Key words we want bolded static $highlight = [ - 'SELECT', - 'DISTINCT', - 'FROM', - 'WHERE', 'AND', - 'LEFT JOIN', - 'RIGHT JOIN', - 'JOIN', - 'ORDER BY', + 'AS', 'ASC', + 'AVG', + 'BY', + 'COUNT', 'DESC', - 'GROUP BY', - 'LIMIT', - 'INSERT', - 'INTO', - 'VALUES', - 'UPDATE', - 'OR', + 'DISTINCT', + 'FROM', + 'GROUP', 'HAVING', - 'OFFSET', - 'NOT IN', 'IN', + 'INNER', + 'INSERT', + 'INTO', + 'IS', + 'JOIN', + 'LEFT', 'LIKE', - 'NOT LIKE', - 'COUNT', + 'LIMIT', 'MAX', 'MIN', + 'NOT', + 'NULL', + 'OFFSET', 'ON', - 'AS', - 'AVG', + 'OR', + 'ORDER', + 'RIGHT', + 'SELECT', 'SUM', + 'UPDATE', + 'VALUES', + 'WHERE', ]; if (empty($this->finalQueryString)) { $this->compileBinds(); // @codeCoverageIgnore } - $sql = $this->finalQueryString; + $sql = esc($this->finalQueryString); - $search = '/\b(?:' . implode('|', $highlight) . ')\b/'; + /** + * @see https://stackoverflow.com/a/20767160 + * @see https://regex101.com/r/hUlrGN/4 + */ + $search = '/\b(?:' . implode('|', $highlight) . ')\b(?![^(')]*'(?:(?:[^(')]*'){2})*[^(')]*$)/'; return preg_replace_callback($search, static function ($matches) { return '' . str_replace(' ', ' ', $matches[0]) . ''; diff --git a/tests/system/Database/BaseQueryTest.php b/tests/system/Database/BaseQueryTest.php index deec43c4f5bb..fe942ffeb187 100644 --- a/tests/system/Database/BaseQueryTest.php +++ b/tests/system/Database/BaseQueryTest.php @@ -363,4 +363,37 @@ public function testSetQueryBinds() $this->assertSame($expected, $query->getQuery()); } + + public function queryKeywords() + { + return [ + 'highlightKeyWords' => [ + 'SELECT `a`.*, `b`.`id` AS `b_id` FROM `a` LEFT JOIN `b` ON `b`.`a_id` = `a`.`id` WHERE `b`.`id` IN ('1') AND `a`.`deleted_at` IS NOT NULL LIMIT 1', + 'SELECT `a`.*, `b`.`id` AS `b_id` FROM `a` LEFT JOIN `b` ON `b`.`a_id` = `a`.`id` WHERE `b`.`id` IN (\'1\') AND `a`.`deleted_at` IS NOT NULL LIMIT 1', + ], + 'ignoreKeyWordsInValues' => [ + 'SELECT * FROM `a` WHERE `a`.`col` = 'SELECT escaped keyword in value' LIMIT 1', + 'SELECT * FROM `a` WHERE `a`.`col` = \'SELECT escaped keyword in value\' LIMIT 1', + ], + 'escapeHtmlValues' => [ + 'SELECT '<s>' FROM dual', + 'SELECT \'\' FROM dual', + ], + ]; + } + + /** + * @dataProvider queryKeywords + * + * @param mixed $expected + * @param mixed $sql + */ + public function testHighlightQueryKeywords($expected, $sql) + { + $query = new Query($this->db); + $query->setQuery($sql); + $query->getQuery(); + + $this->assertSame($expected, $query->debugToolbarDisplay()); + } } From 26e0025df9c508972934fa64cbf1f3be728bc60a Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 17 Oct 2021 00:11:01 +0900 Subject: [PATCH 315/490] Fix CSRF filter does not work when set it to only post * refactor: case the existence of config files with if statements To make it easier to know which parts to delete in the future. * docs: fix PHPDoc explanation * fix: bug that CSRF cookie is not sent just by calling csrf_hash() When the CSRF filter was set to POST method, it did not work. * refactor: replace deprecated method getHeader() * refactor: extract method * refactor: extract method * refactor: extract method * refactor: use $this->hash instead of $_COOKIE * test: fix the timing for setting superglobals * test: fix Cannot modify header information ErrorException: Cannot modify header information - headers already sent by ... * style: vendor/bin/rector process * refactor: ensure instance It becomes clear that it is `SecurityConfig`. Co-authored-by: Abdul Malik Ikhsan * refactor: ensure instance It becomes clear that it is `CookieConfig`. Co-authored-by: Abdul Malik Ikhsan * refactor: when $cookie is null, Cookie::setDefaults($cookie) does nothing * refactor: extract method * fix: make private extracted methods * fix: make private added property * fix: fallback to the local properties Takes care when a user removes properties in config classes. * refactor: use $request instead of $_POST Co-authored-by: Abdul Malik Ikhsan --- app/Config/Security.php | 2 +- system/Security/Security.php | 128 ++++++++++++++------ tests/system/CommonFunctionsTest.php | 3 + tests/system/CommonSingleServiceTest.php | 6 +- tests/system/Config/ServicesTest.php | 2 + tests/system/Helpers/SecurityHelperTest.php | 5 + tests/system/Security/SecurityTest.php | 48 +++++--- 7 files changed, 135 insertions(+), 59 deletions(-) diff --git a/app/Config/Security.php b/app/Config/Security.php index 82afb9396ec4..01000ff52088 100644 --- a/app/Config/Security.php +++ b/app/Config/Security.php @@ -57,7 +57,7 @@ class Security extends BaseConfig * CSRF Regenerate * -------------------------------------------------------------------------- * - * Regenerate CSRF Token on every request. + * Regenerate CSRF Token on every submission. * * @var bool */ diff --git a/system/Security/Security.php b/system/Security/Security.php index 0f508628e240..7bf3b6689095 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -17,6 +17,7 @@ use Config\App; use Config\Cookie as CookieConfig; use Config\Security as SecurityConfig; +use Config\Services; /** * Class Security @@ -77,8 +78,6 @@ class Security implements SecurityInterface * Defaults to two hours (in seconds). * * @var int - * - * @deprecated */ protected $expires = 7200; @@ -117,6 +116,18 @@ class Security implements SecurityInterface */ protected $samesite = Cookie::SAMESITE_LAX; + /** + * @var RequestInterface + */ + private $request; + + /** + * CSRF Cookie Name without Prefix + * + * @var string + */ + private $rawCookieName; + /** * Constructor. * @@ -125,27 +136,46 @@ class Security implements SecurityInterface */ public function __construct(App $config) { - /** @var SecurityConfig $security */ + /** @var SecurityConfig|null $security */ $security = config('Security'); // Store CSRF-related configurations - $this->tokenName = $security->tokenName ?? $config->CSRFTokenName ?? $this->tokenName; - $this->headerName = $security->headerName ?? $config->CSRFHeaderName ?? $this->headerName; - $this->regenerate = $security->regenerate ?? $config->CSRFRegenerate ?? $this->regenerate; - $rawCookieName = $security->cookieName ?? $config->CSRFCookieName ?? $this->cookieName; + if ($security instanceof SecurityConfig) { + $this->tokenName = $security->tokenName ?? $this->tokenName; + $this->headerName = $security->headerName ?? $this->headerName; + $this->regenerate = $security->regenerate ?? $this->regenerate; + $this->rawCookieName = $security->cookieName ?? $this->rawCookieName; + $this->expires = $security->expires ?? $this->expires; + } else { + // `Config/Security.php` is absence + $this->tokenName = $config->CSRFTokenName ?? $this->tokenName; + $this->headerName = $config->CSRFHeaderName ?? $this->headerName; + $this->regenerate = $config->CSRFRegenerate ?? $this->regenerate; + $this->rawCookieName = $config->CSRFCookieName ?? $this->rawCookieName; + $this->expires = $config->CSRFExpire ?? $this->expires; + } - /** @var CookieConfig $cookie */ - $cookie = config('Cookie'); + $this->configureCookie($config); - $cookiePrefix = $cookie->prefix ?? $config->cookiePrefix; - $this->cookieName = $cookiePrefix . $rawCookieName; + $this->request = Services::request(); - $expires = $security->expires ?? $config->CSRFExpire ?? 7200; + $this->generateHash(); + } + + private function configureCookie(App $config): void + { + /** @var CookieConfig|null $cookie */ + $cookie = config('Cookie'); - Cookie::setDefaults($cookie); - $this->cookie = new Cookie($rawCookieName, $this->generateHash(), [ - 'expires' => $expires === 0 ? 0 : time() + $expires, - ]); + if ($cookie instanceof CookieConfig) { + $cookiePrefix = $cookie->prefix; + $this->cookieName = $cookiePrefix . $this->rawCookieName; + Cookie::setDefaults($cookie); + } else { + // `Config/Cookie.php` is absence + $cookiePrefix = $config->cookiePrefix; + $this->cookieName = $cookiePrefix . $this->rawCookieName; + } } /** @@ -202,26 +232,15 @@ public function verify(RequestInterface $request) return $this->sendCookie($request); } - // Does the token exist in POST, HEADER or optionally php:://input - json data. - if ($request->hasHeader($this->headerName) && ! empty($request->getHeader($this->headerName)->getValue())) { - $tokenName = $request->getHeader($this->headerName)->getValue(); - } else { - $json = json_decode($request->getBody()); - - if (! empty($request->getBody()) && ! empty($json) && json_last_error() === JSON_ERROR_NONE) { - $tokenName = $json->{$this->tokenName} ?? null; - } else { - $tokenName = null; - } - } - - $token = $_POST[$this->tokenName] ?? $tokenName; + $token = $this->getPostedToken($request); // Does the tokens exist in both the POST/POSTed JSON and COOKIE arrays and match? - if (! isset($token, $_COOKIE[$this->cookieName]) || ! hash_equals($token, $_COOKIE[$this->cookieName])) { + if (! isset($token, $this->hash) || ! hash_equals($this->hash, $token)) { throw SecurityException::forDisallowedAction(); } + $json = json_decode($request->getBody()); + if (isset($_POST[$this->tokenName])) { // We kill this since we're done and we don't want to pollute the POST array. unset($_POST[$this->tokenName]); @@ -237,14 +256,31 @@ public function verify(RequestInterface $request) unset($_COOKIE[$this->cookieName]); } - $this->cookie = $this->cookie->withValue($this->generateHash()); - $this->sendCookie($request); + $this->generateHash(); log_message('info', 'CSRF token verified.'); return $this; } + private function getPostedToken(RequestInterface $request): ?string + { + // 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(); + } else { + $json = json_decode($request->getBody()); + + if (! empty($request->getBody()) && ! empty($json) && json_last_error() === JSON_ERROR_NONE) { + $tokenName = $json->{$this->tokenName} ?? null; + } else { + $tokenName = null; + } + } + + return $request->getPost($this->tokenName) ?? $tokenName; + } + /** * Returns the CSRF Hash. */ @@ -373,19 +409,37 @@ protected function generateHash(): string // We don't necessarily want to regenerate it with // each page load since a page could contain embedded // sub-pages causing this feature to fail - if (isset($_COOKIE[$this->cookieName]) - && is_string($_COOKIE[$this->cookieName]) - && preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->cookieName]) === 1 - ) { + if ($this->isHashInCookie()) { return $this->hash = $_COOKIE[$this->cookieName]; } $this->hash = bin2hex(random_bytes(16)); + + $this->saveHashInCookie(); } return $this->hash; } + private function isHashInCookie(): bool + { + return isset($_COOKIE[$this->cookieName]) + && is_string($_COOKIE[$this->cookieName]) + && preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->cookieName]) === 1; + } + + private function saveHashInCookie() + { + $this->cookie = new Cookie( + $this->rawCookieName, + $this->hash, + [ + 'expires' => $this->expires === 0 ? 0 : time() + $this->expires, + ] + ); + $this->sendCookie($this->request); + } + /** * CSRF Send Cookie * diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 1ce1e7ab61a2..ad5ae969b1bc 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -22,6 +22,7 @@ use CodeIgniter\Session\Session; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockIncomingRequest; +use CodeIgniter\Test\Mock\MockSecurity; use CodeIgniter\Test\Mock\MockSession; use CodeIgniter\Test\TestLogger; use Config\App; @@ -232,6 +233,8 @@ public function testAppTimezone() public function testCSRFToken() { + Services::injectMock('security', new MockSecurity(new App())); + $this->assertSame('csrf_test_name', csrf_token()); } diff --git a/tests/system/CommonSingleServiceTest.php b/tests/system/CommonSingleServiceTest.php index 1dca6f823ab8..ede9d9b0445e 100644 --- a/tests/system/CommonSingleServiceTest.php +++ b/tests/system/CommonSingleServiceTest.php @@ -13,6 +13,8 @@ use CodeIgniter\Config\Services; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockSecurity; +use Config\App; use ReflectionClass; use ReflectionMethod; @@ -26,6 +28,8 @@ final class CommonSingleServiceTest extends CIUnitTestCase */ public function testSingleServiceWithNoParamsSupplied(string $service): void { + Services::injectMock('security', new MockSecurity(new App())); + $service1 = single_service($service); $service2 = single_service($service); @@ -90,7 +94,7 @@ public static function serviceNamesProvider(): iterable continue; } - yield [$name]; + yield $name => [$name]; } } } diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 6733ee06fff6..2712bdce8dd5 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -397,6 +397,8 @@ public function testRouter() public function testSecurity() { + Services::injectMock('security', new MockSecurity(new App())); + $result = Services::security(); $this->assertInstanceOf(Security::class, $result); } diff --git a/tests/system/Helpers/SecurityHelperTest.php b/tests/system/Helpers/SecurityHelperTest.php index 3e1d8c332080..4d8169f6f712 100644 --- a/tests/system/Helpers/SecurityHelperTest.php +++ b/tests/system/Helpers/SecurityHelperTest.php @@ -12,6 +12,9 @@ namespace CodeIgniter\Helpers; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockSecurity; +use Config\App; +use Tests\Support\Config\Services as Services; /** * @internal @@ -27,6 +30,8 @@ protected function setUp(): void public function testSanitizeFilenameSimpleSuccess() { + Services::injectMock('security', new MockSecurity(new App())); + $this->assertSame('hello.doc', sanitize_filename('hello.doc')); } diff --git a/tests/system/Security/SecurityTest.php b/tests/system/Security/SecurityTest.php index aadb84aa775b..74e8621e3a34 100644 --- a/tests/system/Security/SecurityTest.php +++ b/tests/system/Security/SecurityTest.php @@ -41,7 +41,11 @@ protected function setUp(): void public function testBasicConfigIsSaved() { - $security = new Security(new MockAppConfig()); + $config = new MockAppConfig(); + $security = $this->getMockBuilder(Security::class) + ->setConstructorArgs([$config]) + ->onlyMethods(['doSendCookie']) + ->getMock(); $hash = $security->getHash(); @@ -53,7 +57,11 @@ public function testHashIsReadFromCookie() { $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new Security(new MockAppConfig()); + $config = new MockAppConfig(); + $security = $this->getMockBuilder(Security::class) + ->setConstructorArgs([$config]) + ->onlyMethods(['doSendCookie']) + ->getMock(); $this->assertSame('8b9218a55906f9dcc1dc263dce7f005a', $security->getHash()); } @@ -84,14 +92,14 @@ public function testCSRFVerifyPostThrowsExceptionOnNoMatch() public function testCSRFVerifyPostReturnsSelfOnMatch() { - $security = new MockSecurity(new MockAppConfig()); - $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $_SERVER['REQUEST_METHOD'] = 'POST'; $_POST['foo'] = 'bar'; $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + $security = new MockSecurity(new MockAppConfig()); + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -114,15 +122,15 @@ public function testCSRFVerifyHeaderThrowsExceptionOnNoMatch() public function testCSRFVerifyHeaderReturnsSelfOnMatch() { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['foo'] = 'bar'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + $security = new MockSecurity(new MockAppConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_POST['foo'] = 'bar'; - $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -145,14 +153,14 @@ public function testCSRFVerifyJsonThrowsExceptionOnNoMatch() public function testCSRFVerifyJsonReturnsSelfOnMatch() { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + $security = new MockSecurity(new MockAppConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setBody('{"csrf_test_name":"8b9218a55906f9dcc1dc263dce7f005a","foo":"bar"}'); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -170,6 +178,10 @@ public function testSanitizeFilename() public function testRegenerateWithFalseSecurityRegenerateProperty() { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + $config = new SecurityConfig(); $config->regenerate = false; Factories::injectMock('config', 'Security', $config); @@ -177,10 +189,6 @@ public function testRegenerateWithFalseSecurityRegenerateProperty() $security = new MockSecurity(new MockAppConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $oldHash = $security->getHash(); $security->verify($request); $newHash = $security->getHash(); @@ -190,6 +198,10 @@ public function testRegenerateWithFalseSecurityRegenerateProperty() public function testRegenerateWithTrueSecurityRegenerateProperty() { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + $config = new SecurityConfig(); $config->regenerate = true; Factories::injectMock('config', 'Security', $config); @@ -197,10 +209,6 @@ public function testRegenerateWithTrueSecurityRegenerateProperty() $security = new MockSecurity(new MockAppConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $oldHash = $security->getHash(); $security->verify($request); $newHash = $security->getHash(); From f3424a5062eba1e05edbcb1f609756fdcfcb25f0 Mon Sep 17 00:00:00 2001 From: uraku Date: Sun, 17 Oct 2021 02:05:17 +0900 Subject: [PATCH 316/490] Fix GC issue when session lifetime is set to 0 * fix #4169 GC processing issues when session lifetime is 0 * Added description about the structure of the session library of the document. * Apply suggestions from code review Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com> --- system/Session/Session.php | 2 +- user_guide_src/source/libraries/sessions.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/system/Session/Session.php b/system/Session/Session.php index abeed407425b..ebfb6a94d082 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -306,7 +306,7 @@ protected function configure() if (! isset($this->sessionExpiration)) { $this->sessionExpiration = (int) ini_get('session.gc_maxlifetime'); - } else { + } elseif ($this->sessionExpiration > 0) { ini_set('session.gc_maxlifetime', (string) $this->sessionExpiration); } diff --git a/user_guide_src/source/libraries/sessions.rst b/user_guide_src/source/libraries/sessions.rst index b2efd60b7025..d38ec66e8994 100644 --- a/user_guide_src/source/libraries/sessions.rst +++ b/user_guide_src/source/libraries/sessions.rst @@ -458,6 +458,12 @@ Preference Default Opti unexpected results or be changed in the future. Please configure everything properly. +.. note:: If ``sessionExpiration`` is set to ``0``, the ``session.gc_maxlifetime`` + setting set by PHP in session management will be used as-is + (often the default value of ``1440``). This needs to be changed in + ``php.ini`` or via ``ini_set()`` as needed. + + In addition to the values above, the cookie and native drivers apply the following configuration values shared by the :doc:`IncomingRequest ` and :doc:`Security ` classes: From dfe4ad4c3d344fe272f214f771458e0895060986 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 17 Oct 2021 12:31:38 +0900 Subject: [PATCH 317/490] docs: small improvement in debugging --- user_guide_src/source/testing/debugging.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/testing/debugging.rst b/user_guide_src/source/testing/debugging.rst index 4f6ffdce9e3f..fbd0f8eed440 100644 --- a/user_guide_src/source/testing/debugging.rst +++ b/user_guide_src/source/testing/debugging.rst @@ -19,7 +19,7 @@ and much, much more. Enabling Kint ============= -By default, Kint is enabled in **development** and **testing** environments only. This can be altered by modifying +By default, Kint is enabled in **development** and **testing** :doc:`environments ` only. This can be altered by modifying the ``$useKint`` value in the environment configuration section of the main **index.php** file:: $useKint = true; @@ -59,7 +59,7 @@ to help you debug and optimize. Enabling the Toolbar ==================== -The toolbar is enabled by default in any environment *except* production. It will be shown whenever the +The toolbar is enabled by default in any :doc:`environment ` *except* **production**. It will be shown whenever the constant CI_DEBUG is defined and its value is truthy. This is defined in the boot files (e.g. **app/Config/Boot/development.php**) and can be modified there to determine what environment to show. From 59c79d9a1912c61cdc654bd63109e7c9bdbdc94c Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 17 Oct 2021 17:59:56 +0900 Subject: [PATCH 318/490] fix: test failures on macOS `var` is a symlink to `private/var` on macOS --- system/Publisher/Publisher.php | 1 + tests/system/Publisher/PublisherSupportTest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index 38e990776ef3..7e47c136fd03 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -229,6 +229,7 @@ final public function getScratch(): string if ($this->scratch === null) { $this->scratch = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)) . DIRECTORY_SEPARATOR; mkdir($this->scratch, 0700); + $this->scratch = realpath($this->scratch) . DIRECTORY_SEPARATOR; } return $this->scratch; diff --git a/tests/system/Publisher/PublisherSupportTest.php b/tests/system/Publisher/PublisherSupportTest.php index 43be04406891..83a41b69e991 100644 --- a/tests/system/Publisher/PublisherSupportTest.php +++ b/tests/system/Publisher/PublisherSupportTest.php @@ -144,6 +144,7 @@ public function testWipe() { $directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)); mkdir($directory, 0700); + $directory = realpath($directory); $this->assertDirectoryExists($directory); config('Publisher')->restrictions[$directory] = ''; // Allow the directory From 59ae985f9db44d443b573db1183501d3e8e9caf1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 17 Oct 2021 22:55:21 +0900 Subject: [PATCH 319/490] Add `@group CacheLive` to tests * test: add `@group CacheLive` * docs: add about CacheLive --- tests/README.md | 4 ++-- tests/system/Cache/Handlers/MemcachedHandlerTest.php | 2 ++ tests/system/Cache/Handlers/PredisHandlerTest.php | 2 ++ tests/system/Cache/Handlers/RedisHandlerTest.php | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/README.md b/tests/README.md index 0ae640f24844..438c2674f4bc 100644 --- a/tests/README.md +++ b/tests/README.md @@ -49,9 +49,9 @@ Individual tests can be run by including the relative path to the test file. > ./phpunit tests/system/HTTP/RequestTest.php -You can run the tests without running the live database tests. +You can run the tests without running the live database and the live cache tests. - > ./phpunit --exclude-group DatabaseLive + > ./phpunit --exclude-group DatabaseLive,CacheLive ## Generating Code Coverage diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index af2e0a77775c..240225fd8bc0 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -16,6 +16,8 @@ use Exception; /** + * @group CacheLive + * * @internal */ final class MemcachedHandlerTest extends AbstractHandlerTest diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php index b91ecefa2e2b..9b762f9c27f1 100644 --- a/tests/system/Cache/Handlers/PredisHandlerTest.php +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php @@ -15,6 +15,8 @@ use Config\Cache; /** + * @group CacheLive + * * @internal */ final class PredisHandlerTest extends AbstractHandlerTest diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php index 863d9efa3b59..32d0cdf8223e 100644 --- a/tests/system/Cache/Handlers/RedisHandlerTest.php +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -15,6 +15,8 @@ use Config\Cache; /** + * @group CacheLive + * * @internal */ final class RedisHandlerTest extends AbstractHandlerTest From d10a6029cd766f0a44e468d4f91899dcf99a522e Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 18 Oct 2021 00:23:48 +0900 Subject: [PATCH 320/490] docs: hashed id (`encodeID()`) in CodeIgniter\Model has been removed https://github.com/codeigniter4/CodeIgniter4/commit/f87ffec26b0d3dca7544a6e76e9110f03671dc54 --- user_guide_src/source/incoming/routing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 4bc619f1449f..5740898dd6a6 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -68,7 +68,7 @@ Placeholders Description (:num) will match any integer. (:alpha) will match any string of alphabetic characters (:alphanum) will match any string of alphabetic characters or integers, or any combination of the two. -(:hash) is the same as **(:segment)**, but can be used to easily see which routes use hashed ids (see the :doc:`Model ` docs). +(:hash) is the same as **(:segment)**, but can be used to easily see which routes use hashed ids. ============ =========================================================================================================== .. note:: **{locale}** cannot be used as a placeholder or other part of the route, as it is reserved for use From 52af51d1ffa588fc47aa71f725507ade8726b7df Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 18 Oct 2021 13:56:53 +0900 Subject: [PATCH 321/490] docs: unify the decoration of app/Config/*.php --- user_guide_src/cilexer/pycilexer.egg-info/top_level.txt | 1 + user_guide_src/source/concepts/structure.rst | 2 +- user_guide_src/source/general/logging.rst | 6 +++--- user_guide_src/source/general/modules.rst | 2 +- user_guide_src/source/helpers/cookie_helper.rst | 2 +- user_guide_src/source/helpers/text_helper.rst | 2 +- user_guide_src/source/incoming/controllers.rst | 2 +- user_guide_src/source/incoming/filters.rst | 2 +- user_guide_src/source/incoming/routing.rst | 8 ++++---- .../source/installation/installing_composer.rst | 2 +- user_guide_src/source/installation/troubleshooting.rst | 6 +++--- user_guide_src/source/libraries/cookies.rst | 2 +- user_guide_src/source/libraries/encryption.rst | 6 +++--- user_guide_src/source/libraries/honeypot.rst | 4 ++-- 14 files changed, 24 insertions(+), 23 deletions(-) create mode 100644 user_guide_src/cilexer/pycilexer.egg-info/top_level.txt diff --git a/user_guide_src/cilexer/pycilexer.egg-info/top_level.txt b/user_guide_src/cilexer/pycilexer.egg-info/top_level.txt new file mode 100644 index 000000000000..2f88e1d75272 --- /dev/null +++ b/user_guide_src/cilexer/pycilexer.egg-info/top_level.txt @@ -0,0 +1 @@ +cilexer diff --git a/user_guide_src/source/concepts/structure.rst b/user_guide_src/source/concepts/structure.rst index 40c5f868ee73..14bb3a59304b 100644 --- a/user_guide_src/source/concepts/structure.rst +++ b/user_guide_src/source/concepts/structure.rst @@ -84,6 +84,6 @@ Modifying Directory Locations ----------------------------- If you've relocated any of the main directories, you can change the configuration -settings inside ``app/Config/Paths``. +settings inside **app/Config/Paths.php**. Please read `Managing your Applications <../general/managing_apps.html>`_ diff --git a/user_guide_src/source/general/logging.rst b/user_guide_src/source/general/logging.rst index 6a6ca1e49e0e..7fa706ce6a86 100644 --- a/user_guide_src/source/general/logging.rst +++ b/user_guide_src/source/general/logging.rst @@ -38,7 +38,7 @@ Configuration ============= You can modify which levels are actually logged, as well as assign different Loggers to handle different levels, within -the ``/app/Config/Logger.php`` configuration file. +the **app/Config/Logger.php** configuration file. The ``threshold`` value of the config file determines which levels are logged across your application. If any levels are requested to be logged by the application, but the threshold doesn't allow them to log currently, they will be @@ -140,8 +140,8 @@ You can use any other logger that you might like as long as it extends from eith that you can easily drop in use for any PSR3-compatible logger, or create your own. You must ensure that the third-party logger can be found by the system, by adding it to either -the ``/app/Config/Autoload.php`` configuration file, or through another autoloader, -like Composer. Next, you should modify ``/app/Config/Services.php`` to point the ``logger`` +the **app/Config/Autoload.php** configuration file, or through another autoloader, +like Composer. Next, you should modify **app/Config/Services.php** to point the ``logger`` alias to your new class name. Now, any call that is done through the ``log_message()`` function will use your library instead. diff --git a/user_guide_src/source/general/modules.rst b/user_guide_src/source/general/modules.rst index b744f30c0441..6fc24ecad55d 100644 --- a/user_guide_src/source/general/modules.rst +++ b/user_guide_src/source/general/modules.rst @@ -73,7 +73,7 @@ would be used. Another approach provided by CodeIgniter is to autoload these *non-class* files like how you would autoload your classes. All we need to do is provide the list of paths to those files and include them in the -``$files`` property of your ``app/Config/Autoload.php`` file. +``$files`` property of your **app/Config/Autoload.php** file. :: diff --git a/user_guide_src/source/helpers/cookie_helper.rst b/user_guide_src/source/helpers/cookie_helper.rst index 2a7f0e6e6c43..c7df02f6461f 100755 --- a/user_guide_src/source/helpers/cookie_helper.rst +++ b/user_guide_src/source/helpers/cookie_helper.rst @@ -50,7 +50,7 @@ The following functions are available: detailed description of its use, as this function acts very similarly to ``IncomingRequest::getCookie()``, except it will also prepend the ``$cookiePrefix`` that you might've set in your - *app/Config/App.php* file. + **app/Config/App.php** file. .. php:function:: delete_cookie($name[, $domain = ''[, $path = '/'[, $prefix = '']]]) diff --git a/user_guide_src/source/helpers/text_helper.rst b/user_guide_src/source/helpers/text_helper.rst index 98a6f3b9e332..d83fb54835ae 100755 --- a/user_guide_src/source/helpers/text_helper.rst +++ b/user_guide_src/source/helpers/text_helper.rst @@ -257,7 +257,7 @@ The following functions are available: $string = convert_accented_characters($string); .. note:: This function uses a companion config file - `app/Config/ForeignCharacters.php` to define the to and + **app/Config/ForeignCharacters.php** to define the to and from array for transliteration. .. php:function:: word_censor($str, $censored[, $replacement = '']) diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index dfca82fea06e..516028d8c04d 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -288,7 +288,7 @@ one and place your controller classes within them. Each of your sub-directories may contain a default controller which will be called if the URL contains *only* the sub-directory. Simply put a controller in there that matches the name of your 'default_controller' as specified in -your *app/Config/Routes.php* file. +your **app/Config/Routes.php** file. CodeIgniter also permits you to remap your URIs using its :doc:`URI Routing ` feature. diff --git a/user_guide_src/source/incoming/filters.rst b/user_guide_src/source/incoming/filters.rst index a95333516503..259bb496e0b7 100644 --- a/user_guide_src/source/incoming/filters.rst +++ b/user_guide_src/source/incoming/filters.rst @@ -88,7 +88,7 @@ the final output, or even to filter the final output with a bad words filter. Configuring Filters ******************* -Once you've created your filters, you need to configure when they get run. This is done in ``app/Config/Filters.php``. +Once you've created your filters, you need to configure when they get run. This is done in **app/Config/Filters.php**. This file contains four properties that allow you to configure exactly when the filters run. $aliases diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 5740898dd6a6..7e0e6d462f9b 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -238,7 +238,7 @@ run the filter before or after the controller. This is especially handy during a $routes->resource('users'); }); -The value for the filter must match one of the aliases defined within ``app/Config/Filters.php``. +The value for the filter must match one of the aliases defined within **app/Config/Filters.php**. It is possible to nest groups within groups for finer organization if you need it:: @@ -355,14 +355,14 @@ Applying Filters You can alter the behavior of specific routes by supplying filters to run before or after the controller. This is especially handy during authentication or api logging. The value for the filter can be a string or an array of strings: -* matching the aliases defined in ``app/Config/Filters.php``. +* matching the aliases defined in **app/Config/Filters.php**. * filter classnames See `Controller filters `_ for more information on setting up filters. **Alias filter** -You specify an alias defined in ``app/Config/Filters.php`` for the filter value:: +You specify an alias defined in **app/Config/Filters.php** for the filter value:: $routes->add('admin',' AdminController::index', ['filter' => 'admin-auth']); @@ -477,7 +477,7 @@ Routes Configuration Options ============================ The RoutesCollection class provides several options that affect all routes, and can be modified to meet your -application's needs. These options are available at the top of `/app/Config/Routes.php`. +application's needs. These options are available at the top of **app/Config/Routes.php**. Default Namespace ----------------- diff --git a/user_guide_src/source/installation/installing_composer.rst b/user_guide_src/source/installation/installing_composer.rst index 16e5d1b2015f..b63e34602425 100644 --- a/user_guide_src/source/installation/installing_composer.rst +++ b/user_guide_src/source/installation/installing_composer.rst @@ -140,7 +140,7 @@ Copy the ``env``, ``phpunit.xml.dist`` and ``spark`` files, from ``vendor/codeigniter4/framework`` to your project root You will have to adjust the system path to refer to the vendor one, e.g., ``ROOTPATH . '/vendor/codeigniter4/framework/system'``, -- the ``$systemDirectory`` variable in ``app/Config/Paths.php`` +- the ``$systemDirectory`` variable in **app/Config/Paths.php** Upgrading --------- diff --git a/user_guide_src/source/installation/troubleshooting.rst b/user_guide_src/source/installation/troubleshooting.rst index dc15798d9839..a0b1684fda9d 100644 --- a/user_guide_src/source/installation/troubleshooting.rst +++ b/user_guide_src/source/installation/troubleshooting.rst @@ -30,11 +30,11 @@ Only the default page loads If you find that no matter what you put in your URL only your default page is loading, it might be that your server does not support the REQUEST_URI variable needed to serve search-engine friendly URLs. As a -first step, open your *app/Config/App.php* file and look for +first step, open your **app/Config/App.php** file and look for the URI Protocol information. It will recommend that you try a couple of alternate settings. If it still doesn't work after you've tried this you'll need to force CodeIgniter to add a question mark to your URLs. To -do this open your *app/Config/App.php* file and change this:: +do this open your **app/Config/App.php** file and change this:: public $indexPage = 'index.php'; @@ -72,7 +72,7 @@ Don't forget to reset the environment to "production" once you fix the problem! CodeIgniter Error Logs ---------------------- -CodeIgniter logs error messages, according to the settings in `app/Config/Logger.php`. +CodeIgniter logs error messages, according to the settings in **app/Config/Logger.php**. You can adjust the error threshold to see more or fewer messages. diff --git a/user_guide_src/source/libraries/cookies.rst b/user_guide_src/source/libraries/cookies.rst index e935c56bb1e8..86d36dec9a25 100644 --- a/user_guide_src/source/libraries/cookies.rst +++ b/user_guide_src/source/libraries/cookies.rst @@ -431,7 +431,7 @@ Cookie Personalization Sane defaults are already in place inside the ``Cookie`` class to ensure the smooth creation of cookie objects. However, you may wish to define your own settings by changing the following settings in the -``Config\Cookie`` class in ``app/Config/Cookie.php`` file. +``Config\Cookie`` class in **app/Config/Cookie.php** file. ==================== ===================================== ========= ===================================================== Setting Options/ Types Default Description diff --git a/user_guide_src/source/libraries/encryption.rst b/user_guide_src/source/libraries/encryption.rst index 54fb1fb333a7..2c8373743f35 100644 --- a/user_guide_src/source/libraries/encryption.rst +++ b/user_guide_src/source/libraries/encryption.rst @@ -62,7 +62,7 @@ You don't need to worry about it. Configuring the Library ======================= -The example above uses the configuration settings found in ``app/Config/Encryption.php``. +The example above uses the configuration settings found in **app/Config/Encryption.php**. ========== ==================================================== Option Possible values (default in parentheses) @@ -108,10 +108,10 @@ you can use the Encryption library's ``createKey()`` method. $key = sodium_crypto_secretbox_keygen(); $key = \CodeIgniter\Encryption\Encryption::createKey(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); -The key can be stored in ``app/Config/Encryption.php``, or you can design +The key can be stored in **app/Config/Encryption.php**, or you can design a storage mechanism of your own and pass the key dynamically when encrypting/decrypting. -To save your key to your ``app/Config/Encryption.php``, open the file +To save your key to your **app/Config/Encryption.php**, open the file and set:: public $key = 'YOUR KEY'; diff --git a/user_guide_src/source/libraries/honeypot.rst b/user_guide_src/source/libraries/honeypot.rst index 959a2278178e..471c0d520003 100644 --- a/user_guide_src/source/libraries/honeypot.rst +++ b/user_guide_src/source/libraries/honeypot.rst @@ -14,7 +14,7 @@ assumed the request is coming from a Bot, and you can throw a ``HoneypotExceptio Enabling Honeypot ===================== -To enable a Honeypot, changes have to be made to the ``app/Config/Filters.php``. Just uncomment honeypot +To enable a Honeypot, changes have to be made to the **app/Config/Filters.php**. Just uncomment honeypot from the ``$globals`` array, like...:: public $globals = [ @@ -36,7 +36,7 @@ Customizing Honeypot ===================== Honeypot can be customized. The fields below can be set either in -``app/Config/Honeypot.php`` or in ``.env``. +**app/Config/Honeypot.php** or in ``.env``. * ``hidden`` - true|false to control visibility of the honeypot field; default is ``true`` * ``label`` - HTML label for the honeypot field, default is 'Fill This Field' From a2309e6c8b15840df432da8ed6a50e71eac15163 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 18 Oct 2021 20:55:17 +0900 Subject: [PATCH 322/490] fix: add fallbacks for realpath() so VFS is supported --- system/Publisher/Publisher.php | 3 ++- tests/system/Publisher/PublisherSupportTest.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index 7e47c136fd03..a492a03e02c3 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -229,7 +229,8 @@ final public function getScratch(): string if ($this->scratch === null) { $this->scratch = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)) . DIRECTORY_SEPARATOR; mkdir($this->scratch, 0700); - $this->scratch = realpath($this->scratch) . DIRECTORY_SEPARATOR; + $this->scratch = realpath($this->scratch) ? realpath($this->scratch) . DIRECTORY_SEPARATOR + : $this->scratch . DIRECTORY_SEPARATOR; } return $this->scratch; diff --git a/tests/system/Publisher/PublisherSupportTest.php b/tests/system/Publisher/PublisherSupportTest.php index 83a41b69e991..de1fb29fbd93 100644 --- a/tests/system/Publisher/PublisherSupportTest.php +++ b/tests/system/Publisher/PublisherSupportTest.php @@ -144,7 +144,7 @@ public function testWipe() { $directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)); mkdir($directory, 0700); - $directory = realpath($directory); + $directory = realpath($directory) ?: $directory; $this->assertDirectoryExists($directory); config('Publisher')->restrictions[$directory] = ''; // Allow the directory From 24196f93168af1d5306e0d9b83bddd83a6b1c61e Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 19 Oct 2021 11:34:07 +0900 Subject: [PATCH 323/490] test: remove invalid @runTestsInSeparateProcesses It must be in the class PHPdoc. --- tests/system/Commands/MigrationIntegrationTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/system/Commands/MigrationIntegrationTest.php b/tests/system/Commands/MigrationIntegrationTest.php index f2fca3405354..7dab2f1f8ef1 100644 --- a/tests/system/Commands/MigrationIntegrationTest.php +++ b/tests/system/Commands/MigrationIntegrationTest.php @@ -58,9 +58,6 @@ protected function tearDown(): void stream_filter_remove($this->streamFilter); } - /** - * @runTestsInSeparateProcesses - */ public function testMigrationWithRollbackHasSameNameFormat(): void { command('migrate -n App'); From 34afde80f0435e5157db3d60600ee9d974d9f7ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Oct 2021 15:03:11 +0000 Subject: [PATCH 324/490] chore(deps-dev): update rector/rector requirement Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/0.11.58...0.11.59) --- updated-dependencies: - dependency-name: rector/rector dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8a8fdaf00aa2..bc5e2a6fcaf2 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "phpstan/phpstan": "^0.12.91", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1", - "rector/rector": "0.11.58", + "rector/rector": "0.11.59", "symplify/package-builder": "^9.3" }, "suggest": { From 9718fa0abb12c17cd4597f2edabd3cc4702cef34 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 20 Oct 2021 09:20:24 +0900 Subject: [PATCH 325/490] docs: improve tests/README.md --- tests/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/README.md b/tests/README.md index 438c2674f4bc..a67852b451bd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -8,7 +8,7 @@ use to test your application. Those details can be found in the documentation. ## Requirements It is recommended to use the latest version of PHPUnit. At the time of this -writing we are running version 8.5.13. Support for this has been built into the +writing we are running version 9.5.10. Support for this has been built into the **composer.json** file that ships with CodeIgniter and can easily be installed via [Composer](https://getcomposer.org/) if you don't already have it installed globally. @@ -27,11 +27,11 @@ A number of the tests use a running database. In order to set up the database edit the details for the `tests` group in **app/Config/Database.php** or **phpunit.xml**. Make sure that you provide a database engine that is currently running on your machine. More details on a test database setup are in the -*Docs>>Testing>>Testing Your Database* section of the documentation. +[Testing Your Database](https://codeigniter.com/user_guide/testing/database.html) section of the documentation. If you want to run the tests without using live database you can -exclude @DatabaseLive group. Or make a copy of **phpunit.dist.xml** - -call it **phpunit.xml** - and comment out the named "database". This will make +exclude `@DatabaseLive` group. Or make a copy of **phpunit.dist.xml** - +call it **phpunit.xml** - and comment out the `` named `Database`. This will make the tests run quite a bit faster. ## Running the tests From 7a1cf38eeb04b4eec2ac760f561099823262f19d Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 20 Oct 2021 21:46:42 +0900 Subject: [PATCH 326/490] docs: change the version of PHPUnit to major version only Co-authored-by: MGatner --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index a67852b451bd..0df329864c6b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -8,7 +8,7 @@ use to test your application. Those details can be found in the documentation. ## Requirements It is recommended to use the latest version of PHPUnit. At the time of this -writing we are running version 9.5.10. Support for this has been built into the +writing we are running version 9.x. Support for this has been built into the **composer.json** file that ships with CodeIgniter and can easily be installed via [Composer](https://getcomposer.org/) if you don't already have it installed globally. From ac7d3b6875a121d0626a04c0f59717045ea289b8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 21 Oct 2021 00:32:45 +0900 Subject: [PATCH 327/490] docs: improve Form Validation Tutorial Fixes #3017 --- .../source/libraries/validation.rst | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 940a95953819..d8df45d1c95c 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -150,9 +150,12 @@ To try your form, visit your site using a URL similar to this one:: example.com/index.php/form/ If you submit the form you should simply see the form reload. That's -because you haven't set up any validation rules yet. +because you haven't set up any validation rules in ``$this->validate()`` yet. -.. note:: Since you haven't told the **Validation class** to validate anything +The ``validate()`` method is a method in the Controller. It uses +the **Validation class** inside. See *Validating data* in :doc:`/incoming/controllers`. + +.. note:: Since you haven't told the ``validate()`` method to validate anything yet, it **returns false** (boolean false) **by default**. The ``validate()`` method only returns true if it has successfully applied your rules without any of them failing. @@ -162,9 +165,9 @@ Explanation You'll notice several things about the above pages: -The form (Signup.php) is a standard web form with a couple of exceptions: +The form (**Signup.php**) is a standard web form with a couple of exceptions: -#. It uses a form helper to create the form opening. Technically, this +#. It uses a :doc:`form helper ` to create the form opening. Technically, this isn't necessary. You could create the form using standard HTML. However, the benefit of using the helper is that it generates the action URL for you, based on the URL in your config file. This makes @@ -177,15 +180,31 @@ The form (Signup.php) is a standard web form with a couple of exceptions: This function will return any error messages sent back by the validator. If there are no messages it returns an empty string. -The controller (Form.php) has one method: ``index()``. This method -uses the Controller-provided validate method and loads the form helper and URL +The controller (**Form.php**) has one method: ``index()``. This method +uses the Controller-provided ``validate()`` method and loads the form helper and URL helper used by your view files. It also runs the validation routine. Based on whether the validation was successful it either presents the form or the success page. -Loading the Library +Add Validation Rules ================================================ +Then add validation rules in the controller (**Form.php**):: + + if (! $this->validate([ + 'username' => 'required', + 'password' => 'required|min_length[10]', + 'passconf' => 'required|matches[password]', + 'email' => 'required|valid_email', + ])) { + ... + } + +If you submit the form you should see the success page or the form with error messages. + +Loading the Library +************************************************ + The library is loaded as a service named **validation**:: $validation = \Config\Services::validation(); @@ -197,7 +216,7 @@ for including multiple Rulesets, and collections of rules that can be easily reu the :doc:`Model ` provide methods to make validation even easier. Setting Validation Rules -================================================ +************************************************ CodeIgniter lets you set as many validation rules as you need for a given field, cascading them in order. To set validation rules you @@ -205,7 +224,7 @@ will use the ``setRule()``, ``setRules()``, or ``withRequest()`` methods. setRule() ---------- +========= This method sets a single rule. It takes the name of the field as the first parameter, an optional label and a string with a pipe-delimited list of rules @@ -218,7 +237,7 @@ the data is taken directly from $_POST, then it must be an exact match for the form input name. setRules() ----------- +========== Like, ``setRule()``, but accepts an array of field names and their rules:: @@ -235,7 +254,7 @@ To give a labeled error message you can set up as:: ]); withRequest() -------------- +============= One of the most common times you will use the validation library is when validating data that was input from an HTTP Request. If desired, you can pass an instance of the From eb5d8c2ef8fdfacdc64c3a61a1469742d465e7b6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 21 Oct 2021 00:33:17 +0900 Subject: [PATCH 328/490] docs: improve contributing docs * docs: add link to tests/README.md * docs: remove unneeded line breaks and spaces --- contributing/pull_request.md | 3 ++- contributing/signing.md | 20 +++++++------------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/contributing/pull_request.md b/contributing/pull_request.md index 95e31e676809..d08bbd2e035c 100644 --- a/contributing/pull_request.md +++ b/contributing/pull_request.md @@ -63,7 +63,8 @@ class would test the `Banana` class. There will be occasions when it is more convenient to have separate classes to test different functionality of a single CodeIgniter component. -See the [PHPUnit website](https://phpunit.de/) for more information. +See [Running System Tests](../tests/README.md) +and the [PHPUnit website](https://phpunit.de/) for more information. ### Comments diff --git a/contributing/signing.md b/contributing/signing.md index 0755f6806ed2..ca84ef79a9a7 100644 --- a/contributing/signing.md +++ b/contributing/signing.md @@ -32,25 +32,19 @@ Read below to find out how to sign your commits :) To verify your commits, you will need to setup a GPG key, and attach it to your GitHub account. -See the [git -tools](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work) page -for directions on doing this. The complete story is part of [GitHub -help](https://help.github.com/categories/gpg/). +See the [git tools](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work) page +for directions on doing this. The complete story is part of [GitHub help](https://help.github.com/categories/gpg/). The basic steps are -- [generate your GPG - key](https://help.github.com/articles/generating-a-new-gpg-key/), +- [generate your GPG key](https://help.github.com/articles/generating-a-new-gpg-key/), and copy the ASCII representation of it. -- [Add your GPG key to your GitHub - account](https://help.github.com/articles/adding-a-new-gpg-key-to-your-github-account/). -- [Tell - Git](https://help.github.com/articles/telling-git-about-your-gpg-key/) +- [Add your GPG key to your GitHub account](https://help.github.com/articles/adding-a-new-gpg-key-to-your-github-account/). +- [Tell Git](https://help.github.com/articles/telling-git-about-your-gpg-key/) about your GPG key. -- [Set default - signing](https://help.github.com/articles/signing-commits-using-gpg/) +- [Set default signing](https://help.github.com/articles/signing-commits-using-gpg/) to have all of your commits securely signed automatically. -- Provide your GPG key passphrase, as prompted, when you do a commit. +- Provide your GPG key passphrase, as prompted, when you do a commit. Depending on your IDE, you may have to do your Git commits from your Git bash shell to use the **-S** option to force the secure signing. From 5e03647d34f8f66c2cf7ae7886ca50a8c53f3846 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 21 Oct 2021 00:35:05 +0900 Subject: [PATCH 329/490] test: fix @var classname typo --- tests/system/RESTful/ResourceControllerTest.php | 3 ++- tests/system/RESTful/ResourcePresenterTest.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/system/RESTful/ResourceControllerTest.php b/tests/system/RESTful/ResourceControllerTest.php index 5f2e58994d10..185088da1505 100644 --- a/tests/system/RESTful/ResourceControllerTest.php +++ b/tests/system/RESTful/ResourceControllerTest.php @@ -19,6 +19,7 @@ use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\URI; use CodeIgniter\HTTP\UserAgent; +use CodeIgniter\Router\RouteCollection; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockCodeIgniter; use CodeIgniter\Test\Mock\MockResourceController; @@ -45,7 +46,7 @@ final class ResourceControllerTest extends CIUnitTestCase protected $codeigniter; /** - * @var \CodeIgniter\Router\RoutesCollection + * @var RouteCollection */ protected $routes; diff --git a/tests/system/RESTful/ResourcePresenterTest.php b/tests/system/RESTful/ResourcePresenterTest.php index 2f0a332a57f9..e32ce9aba5d3 100644 --- a/tests/system/RESTful/ResourcePresenterTest.php +++ b/tests/system/RESTful/ResourcePresenterTest.php @@ -13,6 +13,7 @@ use CodeIgniter\CodeIgniter; use CodeIgniter\Config\Services; +use CodeIgniter\Router\RouteCollection; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockCodeIgniter; use CodeIgniter\Test\Mock\MockResourcePresenter; @@ -39,7 +40,7 @@ final class ResourcePresenterTest extends CIUnitTestCase protected $codeigniter; /** - * @var \CodeIgniter\Router\RoutesCollection + * @var RouteCollection */ protected $routes; From 654c2b70770e2603d0c5d37014d5bfa7295d15af Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 21 Oct 2021 00:35:37 +0900 Subject: [PATCH 330/490] docs: add warning about auto-routing and filters --- user_guide_src/source/incoming/routing.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 7e0e6d462f9b..4392ecdba5b1 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -360,6 +360,13 @@ The value for the filter can be a string or an array of strings: See `Controller filters `_ for more information on setting up filters. +.. Warning:: If you set filters to routes in **app/Config/Routes.php** + (not in **app/Config/Filters.php**), it is recommended to disable auto-routing. + When auto-routing is enabled, it may be possible that a controller can be accessed + via a different URL than the configured route, + in which case the filter you specified to the route will not be applied. + See :ref:`use-defined-routes-only` to disable auto-routing. + **Alias filter** You specify an alias defined in **app/Config/Filters.php** for the filter value:: @@ -540,6 +547,8 @@ dash isn’t a valid class or method name character and would cause a fatal erro $routes->setTranslateURIDashes(true); +.. _use-defined-routes-only: + Use Defined Routes Only ----------------------- From 3ea7c9506e19b0c928b51b944592789325104835 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Oct 2021 15:44:27 +0000 Subject: [PATCH 331/490] chore(deps-dev): update rector/rector requirement (#5225) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bc5e2a6fcaf2..d914b452e528 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "phpstan/phpstan": "^0.12.91", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1", - "rector/rector": "0.11.59", + "rector/rector": "0.11.60", "symplify/package-builder": "^9.3" }, "suggest": { From 22634300ebcf39343c310d9288de536e060ba606 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 11:00:39 +0900 Subject: [PATCH 332/490] feat: add Session based CSRF protection --- app/Config/Security.php | 11 + env | 1 + system/Security/Security.php | 76 +++++- .../Security/SecurityCSRFSessionTest.php | 225 ++++++++++++++++++ 4 files changed, 303 insertions(+), 10 deletions(-) create mode 100644 tests/system/Security/SecurityCSRFSessionTest.php diff --git a/app/Config/Security.php b/app/Config/Security.php index 01000ff52088..563cf2f3a86e 100644 --- a/app/Config/Security.php +++ b/app/Config/Security.php @@ -6,6 +6,17 @@ class Security extends BaseConfig { + /** + * -------------------------------------------------------------------------- + * CSRF Protection Method + * -------------------------------------------------------------------------- + * + * Protection Method for Cross Site Request Forgery protection. + * + * @var string 'cookie' or 'session' + */ + public $csrfProtection = 'cookie'; + /** * -------------------------------------------------------------------------- * CSRF Token Name diff --git a/env b/env index 38eabf203988..0b6aa1720782 100644 --- a/env +++ b/env @@ -110,6 +110,7 @@ # SECURITY #-------------------------------------------------------------------- +# security.csrfProtection = 'cookie' # security.tokenName = 'csrf_token_name' # security.headerName = 'X-CSRF-TOKEN' # security.cookieName = 'csrf_cookie_name' diff --git a/system/Security/Security.php b/system/Security/Security.php index 7bf3b6689095..cf55c3aeda5f 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -14,6 +14,7 @@ use CodeIgniter\Cookie\Cookie; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\Security\Exceptions\SecurityException; +use CodeIgniter\Session\Session; use Config\App; use Config\Cookie as CookieConfig; use Config\Security as SecurityConfig; @@ -27,6 +28,18 @@ */ class Security implements SecurityInterface { + public const CSRF_PROTECTION_COOKIE = 'cookie'; + public const CSRF_PROTECTION_SESSION = 'session'; + + /** + * CSRF Protection Method + * + * Protection Method for Cross Site Request Forgery protection. + * + * @var string 'cookie' or 'session' + */ + protected $csrfProtection = self::CSRF_PROTECTION_COOKIE; + /** * CSRF Hash * @@ -128,6 +141,13 @@ class Security implements SecurityInterface */ private $rawCookieName; + /** + * Session instance. + * + * @var Session + */ + private $session; + /** * Constructor. * @@ -141,11 +161,12 @@ public function __construct(App $config) // Store CSRF-related configurations if ($security instanceof SecurityConfig) { - $this->tokenName = $security->tokenName ?? $this->tokenName; - $this->headerName = $security->headerName ?? $this->headerName; - $this->regenerate = $security->regenerate ?? $this->regenerate; - $this->rawCookieName = $security->cookieName ?? $this->rawCookieName; - $this->expires = $security->expires ?? $this->expires; + $this->csrfProtection = $security->csrfProtection ?? $this->csrfProtection; + $this->tokenName = $security->tokenName ?? $this->tokenName; + $this->headerName = $security->headerName ?? $this->headerName; + $this->regenerate = $security->regenerate ?? $this->regenerate; + $this->rawCookieName = $security->cookieName ?? $this->rawCookieName; + $this->expires = $security->expires ?? $this->expires; } else { // `Config/Security.php` is absence $this->tokenName = $config->CSRFTokenName ?? $this->tokenName; @@ -155,13 +176,28 @@ public function __construct(App $config) $this->expires = $config->CSRFExpire ?? $this->expires; } - $this->configureCookie($config); + if ($this->isCSRFCookie()) { + $this->configureCookie($config); + } else { + // Session based CSRF protection + $this->configureSession(); + } $this->request = Services::request(); $this->generateHash(); } + private function isCSRFCookie(): bool + { + return $this->csrfProtection === self::CSRF_PROTECTION_COOKIE; + } + + private function configureSession(): void + { + $this->session = Services::session(); + } + private function configureCookie(App $config): void { /** @var CookieConfig|null $cookie */ @@ -253,7 +289,12 @@ public function verify(RequestInterface $request) if ($this->regenerate) { $this->hash = null; - unset($_COOKIE[$this->cookieName]); + if ($this->isCSRFCookie()) { + unset($_COOKIE[$this->cookieName]); + } else { + // Session based CSRF protection + $this->session->remove($this->tokenName); + } } $this->generateHash(); @@ -409,13 +450,23 @@ protected function generateHash(): string // We don't necessarily want to regenerate it with // each page load since a page could contain embedded // sub-pages causing this feature to fail - if ($this->isHashInCookie()) { - return $this->hash = $_COOKIE[$this->cookieName]; + if ($this->isCSRFCookie()) { + if ($this->isHashInCookie()) { + return $this->hash = $_COOKIE[$this->cookieName]; + } + } elseif ($this->session->has($this->tokenName)) { + // Session based CSRF protection + return $this->hash = $this->session->get($this->tokenName); } $this->hash = bin2hex(random_bytes(16)); - $this->saveHashInCookie(); + if ($this->isCSRFCookie()) { + $this->saveHashInCookie(); + } else { + // Session based CSRF protection + $this->saveHashInSession(); + } } return $this->hash; @@ -467,4 +518,9 @@ protected function doSendCookie(): void { cookies([$this->cookie], false)->dispatch(); } + + private function saveHashInSession(): void + { + $this->session->set($this->tokenName, $this->hash); + } } diff --git a/tests/system/Security/SecurityCSRFSessionTest.php b/tests/system/Security/SecurityCSRFSessionTest.php new file mode 100644 index 000000000000..040f253b9774 --- /dev/null +++ b/tests/system/Security/SecurityCSRFSessionTest.php @@ -0,0 +1,225 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Security; + +use CodeIgniter\Config\Factories; +use CodeIgniter\Config\Services; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\URI; +use CodeIgniter\HTTP\UserAgent; +use CodeIgniter\Security\Exceptions\SecurityException; +use CodeIgniter\Session\Handlers\ArrayHandler; +use CodeIgniter\Session\Session; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockAppConfig; +use CodeIgniter\Test\Mock\MockSession; +use CodeIgniter\Test\TestLogger; +use Config\App as AppConfig; +use Config\Logger as LoggerConfig; +use Config\Security as SecurityConfig; + +/** + * @runTestsInSeparateProcesses + * @preserveGlobalState disabled + * + * @internal + */ +final class SecurityCSRFSessionTest extends CIUnitTestCase +{ + /** + * @var string CSRF protection hash + */ + private $hash = '8b9218a55906f9dcc1dc263dce7f005a'; + + protected function setUp(): void + { + parent::setUp(); + + $_SESSION = []; + Factories::reset(); + + $config = new SecurityConfig(); + $config->csrfProtection = Security::CSRF_PROTECTION_SESSION; + Factories::injectMock('config', 'Security', $config); + + $this->injectSession($this->hash); + } + + private function createSession($options = []): Session + { + $defaults = [ + 'sessionDriver' => 'CodeIgniter\Session\Handlers\FileHandler', + 'sessionCookieName' => 'ci_session', + 'sessionExpiration' => 7200, + 'sessionSavePath' => null, + 'sessionMatchIP' => false, + 'sessionTimeToUpdate' => 300, + 'sessionRegenerateDestroy' => false, + 'cookieDomain' => '', + 'cookiePrefix' => '', + 'cookiePath' => '/', + 'cookieSecure' => false, + 'cookieSameSite' => 'Lax', + ]; + + $config = array_merge($defaults, $options); + $appConfig = new AppConfig(); + + foreach ($config as $key => $c) { + $appConfig->{$key} = $c; + } + + $session = new MockSession(new ArrayHandler($appConfig, '127.0.0.1'), $appConfig); + $session->setLogger(new TestLogger(new LoggerConfig())); + + return $session; + } + + private function injectSession(string $hash): void + { + $session = $this->createSession(); + $session->set('csrf_test_name', $hash); + Services::injectMock('session', $session); + } + + public function testHashIsReadFromSession() + { + $security = new Security(new MockAppConfig()); + + $this->assertSame($this->hash, $security->getHash()); + } + + public function testCSRFVerifyPostThrowsExceptionOnNoMatch() + { + $this->expectException(SecurityException::class); + + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; + + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + + $security = new Security(new MockAppConfig()); + + $security->verify($request); + } + + public function testCSRFVerifyPostReturnsSelfOnMatch() + { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['foo'] = 'bar'; + $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + + $security = new Security(new MockAppConfig()); + + $this->assertInstanceOf(Security::class, $security->verify($request)); + $this->assertLogged('info', 'CSRF token verified.'); + $this->assertTrue(count($_POST) === 1); + } + + public function testCSRFVerifyHeaderThrowsExceptionOnNoMatch() + { + $_SERVER['REQUEST_METHOD'] = 'POST'; + + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005b'); + + $security = new Security(new MockAppConfig()); + + $this->expectException(SecurityException::class); + $security->verify($request); + } + + public function testCSRFVerifyHeaderReturnsSelfOnMatch() + { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['foo'] = 'bar'; + + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); + + $security = new Security(new MockAppConfig()); + + $this->assertInstanceOf(Security::class, $security->verify($request)); + $this->assertLogged('info', 'CSRF token verified.'); + $this->assertCount(1, $_POST); + } + + public function testCSRFVerifyJsonThrowsExceptionOnNoMatch() + { + $this->expectException(SecurityException::class); + + $_SERVER['REQUEST_METHOD'] = 'POST'; + + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + $request->setBody('{"csrf_test_name":"8b9218a55906f9dcc1dc263dce7f005b"}'); + + $security = new Security(new MockAppConfig()); + + $security->verify($request); + } + + public function testCSRFVerifyJsonReturnsSelfOnMatch() + { + $_SERVER['REQUEST_METHOD'] = 'POST'; + + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + $request->setBody('{"csrf_test_name":"8b9218a55906f9dcc1dc263dce7f005a","foo":"bar"}'); + + $security = new Security(new MockAppConfig()); + + $this->assertInstanceOf(Security::class, $security->verify($request)); + $this->assertLogged('info', 'CSRF token verified.'); + $this->assertTrue($request->getBody() === '{"foo":"bar"}'); + } + + public function testRegenerateWithFalseSecurityRegenerateProperty() + { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + + $config = Factories::config('Security'); + $config->regenerate = false; + Factories::injectMock('config', 'Security', $config); + + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + + $security = new Security(new MockAppConfig()); + + $oldHash = $security->getHash(); + $security->verify($request); + $newHash = $security->getHash(); + + $this->assertSame($oldHash, $newHash); + } + + public function testRegenerateWithTrueSecurityRegenerateProperty() + { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + + $config = Factories::config('Security'); + $config->regenerate = true; + Factories::injectMock('config', 'Security', $config); + + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + + $security = new Security(new MockAppConfig()); + + $oldHash = $security->getHash(); + $security->verify($request); + $newHash = $security->getHash(); + + $this->assertNotSame($oldHash, $newHash); + } +} From 991c412adc6a9ba733474b94059fa4c558ec28c0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 11:01:48 +0900 Subject: [PATCH 333/490] refactor: add missing return type --- system/Security/Security.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Security/Security.php b/system/Security/Security.php index cf55c3aeda5f..d6edbb23025a 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -479,7 +479,7 @@ private function isHashInCookie(): bool && preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->cookieName]) === 1; } - private function saveHashInCookie() + private function saveHashInCookie(): void { $this->cookie = new Cookie( $this->rawCookieName, From 537867a1471a3b8a360662a4a6df13dffb2d0f84 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 13:04:26 +0900 Subject: [PATCH 334/490] docs: add Session based CSRF Protection --- user_guide_src/source/libraries/security.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index 9497723c4b46..3b48072fd84d 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -120,6 +120,22 @@ than simply crashing. This can be turned off by editing the following config par Even when the redirect value is **true**, AJAX calls will not redirect, but will throw an error. +======================= +CSRF Protection Methods +======================= + +By default, the Cookie based CSRF Protection is used. It is +`Double Submit Cookie `_ +on OWASP Cross-Site Request Forgery Prevention Cheat Sheet. + +You can also use Session based CSRF Protection. It is +`Synchronizer Token Pattern `_. + +You can set to use the Session based CSRF protection by editing the following config parameter value in +**app/Config/Security.php**:: + + public $csrfProtection = 'session'; + ********************* Other Helpful Methods ********************* From b2fb16a07e9f65f54139cfb36a9b7d59eb7bebff Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 12 Oct 2021 14:12:11 +0900 Subject: [PATCH 335/490] docs: fix text decoration --- user_guide_src/source/libraries/security.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index 3b48072fd84d..d788e2d2a808 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -118,7 +118,7 @@ than simply crashing. This can be turned off by editing the following config par public $redirect = false; -Even when the redirect value is **true**, AJAX calls will not redirect, but will throw an error. +Even when the redirect value is ``true``, AJAX calls will not redirect, but will throw an error. ======================= CSRF Protection Methods @@ -148,8 +148,8 @@ you might find helpful that are not related to the CSRF protection. Tries to sanitize filenames in order to prevent directory traversal attempts and other security threats, which is particularly useful for files that were supplied via user input. The first parameter is the path to sanitize. -If it is acceptable for the user input to include relative paths, e.g., file/in/some/approved/folder.txt, you can set -the second optional parameter, $relative_path to true. +If it is acceptable for the user input to include relative paths, e.g., **file/in/some/approved/folder.txt**, you can set +the second optional parameter, ``$relativePath`` to ``true``. :: $path = $security->sanitizeFilename($request->getVar('filepath')); From 985134458b6a9eaf68be3ae0da02bc30a998bec7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 13 Oct 2021 09:36:51 +0900 Subject: [PATCH 336/490] chore: Security can depend on Session For Session based CSRF Protection --- depfile.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/depfile.yaml b/depfile.yaml index c1d8b87c3db2..2c87969f4fc4 100644 --- a/depfile.yaml +++ b/depfile.yaml @@ -193,6 +193,7 @@ ruleset: - HTTP Security: - Cookie + - Session - HTTP Session: - Cookie From 562c12ef84a5a29f8eac5ff5e086696d5a18d9f1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 22 Oct 2021 09:39:25 +0900 Subject: [PATCH 337/490] docs: update link for CSRF protection Now Security class provides it. --- user_guide_src/source/concepts/security.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/concepts/security.rst b/user_guide_src/source/concepts/security.rst index d13c47d73564..63581c9510dd 100644 --- a/user_guide_src/source/concepts/security.rst +++ b/user_guide_src/source/concepts/security.rst @@ -57,7 +57,7 @@ CodeIgniter provisions ---------------------- - `Session <../libraries/sessions.html>`_ library -- `HTTP library <../incoming/incomingrequest.html>`_ provides for CSRF validation +- :doc:`Security ` library provides for CSRF validation - Easy to add third party authentication ***************************** @@ -162,7 +162,7 @@ CodeIgniter provisions ---------------------- - Public folder, with application and system outside -- `HTTP library <../incoming/incomingrequest.html>`_ provides for CSRF validation +- :doc:`Security ` library provides for CSRF validation ************************************ A8 Cross Site Request Forgery (CSRF) @@ -181,7 +181,7 @@ OWASP recommendations CodeIgniter provisions ---------------------- -- `HTTP library <../incoming/incomingrequest.html>`_ provides for CSRF validation +- :doc:`Security ` library provides for CSRF validation ********************************************** A9 Using Components with Known Vulnerabilities From 010fa6880792c4cc9fe23544d9f4ed10fde62d18 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 22 Oct 2021 09:42:54 +0900 Subject: [PATCH 338/490] docs: fix sample code The CSRF filter alias name is lower case. --- user_guide_src/source/libraries/throttler.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/throttler.rst b/user_guide_src/source/libraries/throttler.rst index f4fbc9a325f4..bc13c16fc321 100644 --- a/user_guide_src/source/libraries/throttler.rst +++ b/user_guide_src/source/libraries/throttler.rst @@ -117,7 +117,7 @@ filter:: Next, we assign it to all POST requests made on the site:: public $methods = [ - 'post' => ['throttle', 'CSRF'], + 'post' => ['throttle', 'csrf'], ]; And that's all there is to it. Now all POST requests made on the site will have to be rate limited. From 045c6ea0915b3724b25ff5b7125f8d978a0d47cd Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 22 Oct 2021 09:59:15 +0900 Subject: [PATCH 339/490] docs: remove Config\App.CSRFProtection which does not exist --- user_guide_src/source/general/configuration.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/user_guide_src/source/general/configuration.rst b/user_guide_src/source/general/configuration.rst index d5599e1a0f25..194079ef9995 100644 --- a/user_guide_src/source/general/configuration.rst +++ b/user_guide_src/source/general/configuration.rst @@ -165,7 +165,6 @@ the configuration class properties are left unchanged. In this usage, the prefix the full (case-sensitive) namespace of the class. :: - Config\App.CSRFProtection = true Config\App.CSRFCookieName = csrf_cookie Config\App.CSPEnabled = true @@ -177,7 +176,6 @@ the configuration class name. If the short prefix matches the class name, the value from **.env** replaces the configuration file value. :: - app.CSRFProtection = true app.CSRFCookieName = csrf_cookie app.CSPEnabled = true @@ -186,7 +184,6 @@ the value from **.env** replaces the configuration file value. Some environments do not permit variable name with dots. In such case, you could also use ``_`` as a seperator. :: - app_CSRFProtection = true app_CSRFCookieName = csrf_cookie app_CSPEnabled = true From 485a0c3694be1c9cb9951eca8aa5740644c27756 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 22 Oct 2021 10:03:33 +0900 Subject: [PATCH 340/490] docs: replace deprecated Config\App.CSRFCookieName --- user_guide_src/source/general/configuration.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/general/configuration.rst b/user_guide_src/source/general/configuration.rst index 194079ef9995..8254b9fe44ea 100644 --- a/user_guide_src/source/general/configuration.rst +++ b/user_guide_src/source/general/configuration.rst @@ -165,7 +165,7 @@ the configuration class properties are left unchanged. In this usage, the prefix the full (case-sensitive) namespace of the class. :: - Config\App.CSRFCookieName = csrf_cookie + Config\App.forceGlobalSecureRequests = true Config\App.CSPEnabled = true @@ -176,7 +176,7 @@ the configuration class name. If the short prefix matches the class name, the value from **.env** replaces the configuration file value. :: - app.CSRFCookieName = csrf_cookie + app.forceGlobalSecureRequests = true app.CSPEnabled = true .. note:: When using the *short prefix* the property names must still exactly match the class defined name. @@ -184,7 +184,7 @@ the value from **.env** replaces the configuration file value. Some environments do not permit variable name with dots. In such case, you could also use ``_`` as a seperator. :: - app_CSRFCookieName = csrf_cookie + app_forceGlobalSecureRequests = true app_CSPEnabled = true Environment Variables as Replacements for Data From e3eea9470beaa459dbc949ce3530438bf97ca913 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 22 Oct 2021 10:10:37 +0900 Subject: [PATCH 341/490] docs: make @deprecated IncomingRequest::$enableCSRF that is not used --- system/HTTP/IncomingRequest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 21894949cb29..128d6ee1c432 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -51,6 +51,8 @@ class IncomingRequest extends Request * Set automatically based on Config setting. * * @var bool + * + * @deprecated Not used */ protected $enableCSRF = false; From 5d8724903c3190db7402f44d18c268183f397e8f Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 22 Oct 2021 10:11:54 +0900 Subject: [PATCH 342/490] test: remove $CSRFProtection properties in Mock*Config.php It does not exist. --- system/Test/Mock/MockAppConfig.php | 1 - system/Test/Mock/MockCLIConfig.php | 1 - 2 files changed, 2 deletions(-) diff --git a/system/Test/Mock/MockAppConfig.php b/system/Test/Mock/MockAppConfig.php index fe51c4d3202f..15aff95809f0 100644 --- a/system/Test/Mock/MockAppConfig.php +++ b/system/Test/Mock/MockAppConfig.php @@ -24,7 +24,6 @@ class MockAppConfig extends App public $cookieHTTPOnly = false; public $cookieSameSite = 'Lax'; public $proxyIPs = ''; - public $CSRFProtection = false; public $CSRFTokenName = 'csrf_test_name'; public $CSRFHeaderName = 'X-CSRF-TOKEN'; public $CSRFCookieName = 'csrf_cookie_name'; diff --git a/system/Test/Mock/MockCLIConfig.php b/system/Test/Mock/MockCLIConfig.php index 0e5f9c8dd64e..6eb0dd70a8b8 100644 --- a/system/Test/Mock/MockCLIConfig.php +++ b/system/Test/Mock/MockCLIConfig.php @@ -24,7 +24,6 @@ class MockCLIConfig extends App public $cookieHTTPOnly = false; public $cookieSameSite = 'Lax'; public $proxyIPs = ''; - public $CSRFProtection = false; public $CSRFTokenName = 'csrf_test_name'; public $CSRFCookieName = 'csrf_cookie_name'; public $CSRFExpire = 7200; From 2a9ab6a94bdf481a073877f6236e7cefe51af0c2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 22 Oct 2021 17:08:47 +0900 Subject: [PATCH 343/490] docs: remove inappropriate samples Validation library does not trim the data unlike CI3's. Generally, HTML encoding should be done in Views. --- user_guide_src/source/libraries/validation.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index d8df45d1c95c..60cbec369b26 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -33,8 +33,7 @@ On the receiving end, the script must: be someone else's existing username, or perhaps even a reserved word. Etc. #. Sanitize the data for security. -#. Pre-format the data if needed (Does the data need to be trimmed? HTML - encoded? Etc.) +#. Pre-format the data if needed. #. Prep the data for insertion in the database. Although there is nothing terribly complex about the above process, it From 02a747ff590c6ffe9aeed99725daee1ad261fb7b Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 22 Oct 2021 17:33:00 +0900 Subject: [PATCH 344/490] docs: decorate variables --- user_guide_src/source/libraries/validation.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 60cbec369b26..10aa4cbd1135 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -232,7 +232,7 @@ that should be applied:: $validation->setRule('username', 'Username', 'required'); The **field name** must match the key of any data array that is sent in. If -the data is taken directly from $_POST, then it must be an exact match for +the data is taken directly from ``$_POST``, then it must be an exact match for the form input name. setRules() @@ -439,7 +439,7 @@ Validation Placeholders The Validation class provides a simple method to replace parts of your rules based on data that's being passed into it. This sounds fairly obscure but can be especially handy with the ``is_unique`` validation rule. Placeholders are simply -the name of the field (or array key) that was passed in as $data surrounded by curly brackets. It will be +the name of the field (or array key) that was passed in as ``$data`` surrounded by curly brackets. It will be replaced by the **value** of the matched incoming field. An example should clarify this:: $validation->setRules([ @@ -686,7 +686,7 @@ a boolean true or false value signifying true if it passed the test or false if } By default, the system will look within ``CodeIgniter\Language\en\Validation.php`` for the language strings used -within errors. In custom rules, you may provide error messages by accepting a $error variable by reference in the +within errors. In custom rules, you may provide error messages by accepting a ``$error`` variable by reference in the second parameter:: public function even(string $str, string &$error = null): bool @@ -710,7 +710,7 @@ Allowing Parameters =================== If your method needs to work with parameters, the function will need a minimum of three parameters: the string to validate, -the parameter string, and an array with all of the data that was submitted the form. The $data array is especially handy +the parameter string, and an array with all of the data that was submitted the form. The ``$data`` array is especially handy for rules like ``require_with`` that needs to check the value of another submitted field to base its result on:: public function required_with($str, string $fields, array $data): bool From 09612db82d36c5011172c04f695410668954f8b6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 22 Oct 2021 17:33:57 +0900 Subject: [PATCH 345/490] docs: fix descrition aboug PHP native functions When using native functions, it needs one or three params. $rule($value) or $rule($value, $param, $data). It seems there is no function to fit the API. CI3's validation alters data. CI4 does not. --- user_guide_src/source/libraries/validation.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 10aa4cbd1135..1779877c50b1 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -928,6 +928,6 @@ is_image Yes Fails if the file cannot be determined to be The file validation rules apply for both single and multiple file uploads. -.. note:: You can also use any native PHP functions that permit up - to two parameters, where at least one is required (to pass - the field data). +.. note:: You can also use any native PHP functions that return boolean and + permit at least one parameter, the field data to validate. + The Validation library **never alters the data** to validate. From fb30326e2a7c579fb7b9ff29557c4576e49e02c7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 21 Oct 2021 15:09:09 +0900 Subject: [PATCH 346/490] docs: fix comment --- system/Security/Security.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Security/Security.php b/system/Security/Security.php index d6edbb23025a..fc472fec6e99 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -270,7 +270,7 @@ public function verify(RequestInterface $request) $token = $this->getPostedToken($request); - // Does the tokens exist in both the POST/POSTed JSON and COOKIE arrays and match? + // Do the tokens match? if (! isset($token, $this->hash) || ! hash_equals($this->hash, $token)) { throw SecurityException::forDisallowedAction(); } From d67de2017ea7d2e1f3590b7fae9651aaa85bf289 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 21 Oct 2021 15:11:02 +0900 Subject: [PATCH 347/490] feat: protect CSRF for PUT/PATCH/DELETE Fixes #2913 --- system/Security/Security.php | 10 ++++--- .../Security/SecurityCSRFSessionTest.php | 30 +++++++++++++++++-- tests/system/Security/SecurityTest.php | 2 +- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/system/Security/Security.php b/system/Security/Security.php index fc472fec6e99..01dd92b33c19 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -259,13 +259,15 @@ public function getCSRFTokenName(): string * * @throws SecurityException * - * @return $this|false + * @return $this */ public function verify(RequestInterface $request) { - // If it's not a POST request we will set the CSRF cookie. - if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST') { - return $this->sendCookie($request); + // Protects POST, PUT, DELETE, PATCH + $method = strtoupper($_SERVER['REQUEST_METHOD']); + $methodsToProtect = ['POST', 'PUT', 'DELETE', 'PATCH']; + if (! in_array($method, $methodsToProtect, true)) { + return $this; } $token = $this->getPostedToken($request); diff --git a/tests/system/Security/SecurityCSRFSessionTest.php b/tests/system/Security/SecurityCSRFSessionTest.php index 040f253b9774..c0ec2c066c40 100644 --- a/tests/system/Security/SecurityCSRFSessionTest.php +++ b/tests/system/Security/SecurityCSRFSessionTest.php @@ -127,7 +127,7 @@ public function testCSRFVerifyPostReturnsSelfOnMatch() $this->assertTrue(count($_POST) === 1); } - public function testCSRFVerifyHeaderThrowsExceptionOnNoMatch() + public function testCSRFVerifyPOSTHeaderThrowsExceptionOnNoMatch() { $_SERVER['REQUEST_METHOD'] = 'POST'; @@ -140,7 +140,7 @@ public function testCSRFVerifyHeaderThrowsExceptionOnNoMatch() $security->verify($request); } - public function testCSRFVerifyHeaderReturnsSelfOnMatch() + public function testCSRFVerifyPOSTHeaderReturnsSelfOnMatch() { $_SERVER['REQUEST_METHOD'] = 'POST'; $_POST['foo'] = 'bar'; @@ -155,6 +155,32 @@ public function testCSRFVerifyHeaderReturnsSelfOnMatch() $this->assertCount(1, $_POST); } + public function testCSRFVerifyPUTHeaderThrowsExceptionOnNoMatch() + { + $_SERVER['REQUEST_METHOD'] = 'PUT'; + + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005b'); + + $security = new Security(new MockAppConfig()); + + $this->expectException(SecurityException::class); + $security->verify($request); + } + + public function testCSRFVerifyPUTHeaderReturnsSelfOnMatch() + { + $_SERVER['REQUEST_METHOD'] = 'PUT'; + + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); + + $security = new Security(new MockAppConfig()); + + $this->assertInstanceOf(Security::class, $security->verify($request)); + $this->assertLogged('info', 'CSRF token verified.'); + } + public function testCSRFVerifyJsonThrowsExceptionOnNoMatch() { $this->expectException(SecurityException::class); diff --git a/tests/system/Security/SecurityTest.php b/tests/system/Security/SecurityTest.php index 74e8621e3a34..eadcf5ff6326 100644 --- a/tests/system/Security/SecurityTest.php +++ b/tests/system/Security/SecurityTest.php @@ -66,7 +66,7 @@ public function testHashIsReadFromCookie() $this->assertSame('8b9218a55906f9dcc1dc263dce7f005a', $security->getHash()); } - public function testCSRFVerifySetsCookieWhenNotPOST() + public function testGetHashSetsCookieWhenNotPOST() { $security = new MockSecurity(new MockAppConfig()); From adf02f0c2583139fee86869032485ef8e83251b6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 21 Oct 2021 16:22:10 +0900 Subject: [PATCH 348/490] fix: HTTP Method Spoofing does not work correctly for Config\Filter::$methods and CSRF protection --- system/Filters/Filters.php | 2 +- system/Security/Security.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index c81dadf3330f..70cf58986727 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -443,7 +443,7 @@ protected function processMethods() } // Request method won't be set for CLI-based requests - $method = strtolower($_SERVER['REQUEST_METHOD'] ?? 'cli'); + $method = strtolower($this->request->getMethod()) ?? 'cli'; if (array_key_exists($method, $this->config->methods)) { $this->filters['before'] = array_merge($this->filters['before'], $this->config->methods[$method]); diff --git a/system/Security/Security.php b/system/Security/Security.php index 01dd92b33c19..d9a44c4c9e6a 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -264,7 +264,7 @@ public function getCSRFTokenName(): string public function verify(RequestInterface $request) { // Protects POST, PUT, DELETE, PATCH - $method = strtoupper($_SERVER['REQUEST_METHOD']); + $method = strtoupper($request->getMethod()); $methodsToProtect = ['POST', 'PUT', 'DELETE', 'PATCH']; if (! in_array($method, $methodsToProtect, true)) { return $this; From f44ceba98c7586eb933abb20d079cd94f6435c6c Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 21 Oct 2021 18:17:38 +0900 Subject: [PATCH 349/490] test: fix test CLIRequest should be used when CLI testing --- tests/system/Filters/FiltersTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index dbe88c4879c5..1b8b4b27d956 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -13,8 +13,10 @@ use CodeIgniter\Config\Services; use CodeIgniter\Filters\Exceptions\FilterException; +use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockAppConfig; require_once __DIR__ . '/fixtures/GoogleMe.php'; require_once __DIR__ . '/fixtures/GoogleYou.php'; @@ -60,7 +62,8 @@ public function testProcessMethodDetectsCLI() 'cli' => ['foo'], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); + $request = new CLIRequest(new MockAppConfig()); + $filters = new Filters((object) $config, $request, $this->response); $expected = [ 'before' => ['foo'], From fa78859639c6de7243db4975ea82deacb8b751db Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 21 Oct 2021 18:19:31 +0900 Subject: [PATCH 350/490] test: fix tests Request object should be created after setting super globals. --- tests/system/Filters/FiltersTest.php | 174 +++++++++++++++------------ 1 file changed, 94 insertions(+), 80 deletions(-) diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index 1b8b4b27d956..dae4074cd134 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -50,7 +50,6 @@ protected function setUp(): void Services::autoloader()->addNamespace($defaults); - $this->request = Services::request(); $this->response = Services::response(); } @@ -83,7 +82,8 @@ public function testProcessMethodDetectsGetRequests() 'get' => ['foo'], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $expected = [ 'before' => ['foo'], @@ -107,7 +107,8 @@ public function testProcessMethodRespectsMethod() 'get' => ['bar'], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $expected = [ 'before' => ['bar'], @@ -131,7 +132,8 @@ public function testProcessMethodIgnoresMethod() 'get' => ['bar'], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $expected = [ 'before' => [], @@ -161,8 +163,8 @@ public function testProcessMethodProcessGlobals() ], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $expected = [ 'before' => [ @@ -210,8 +212,9 @@ public function testProcessMethodProcessGlobalsWithExcept(array $except) ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $expected = [ 'before' => [ @@ -240,8 +243,9 @@ public function testProcessMethodProcessesFiltersBefore() ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $expected = [ 'before' => ['foo'], @@ -268,8 +272,9 @@ public function testProcessMethodProcessesFiltersAfter() ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'users/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'users/foo/bar'; $expected = [ 'before' => [], @@ -314,8 +319,9 @@ public function testProcessMethodProcessesCombined() ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $expected = [ 'before' => [ @@ -355,8 +361,9 @@ public function testProcessMethodProcessesCombinedAfterForToolbar() ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $expected = [ 'before' => ['bar'], @@ -381,8 +388,8 @@ public function testRunThrowsWithInvalidAlias() 'after' => [], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $this->expectException(FilterException::class); $uri = 'admin/foo/bar'; @@ -401,9 +408,9 @@ public function testCustomFiltersLoad() 'after' => [], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $request = $filters->run($uri, 'before'); @@ -421,8 +428,8 @@ public function testRunThrowsWithInvalidClassType() 'after' => [], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $this->expectException(FilterException::class); $uri = 'admin/foo/bar'; @@ -441,9 +448,9 @@ public function testRunDoesBefore() 'after' => [], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $request = $filters->run($uri, 'before'); @@ -461,9 +468,9 @@ public function testRunDoesAfter() 'after' => ['google'], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $response = $filters->run($uri, 'after'); @@ -481,9 +488,9 @@ public function testShortCircuit() 'after' => [], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $response = $filters->run($uri, 'before'); $this->assertTrue($response instanceof ResponseInterface); @@ -507,9 +514,9 @@ public function testOtherResult() 'after' => [], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $response = $filters->run($uri, 'before'); @@ -536,8 +543,9 @@ public function testBeforeExceptString() ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $expected = [ 'before' => [ @@ -569,8 +577,9 @@ public function testBeforeExceptInapplicable() ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $expected = [ 'before' => [ @@ -603,8 +612,9 @@ public function testAfterExceptString() ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $expected = [ 'before' => [ @@ -636,8 +646,9 @@ public function testAfterExceptInapplicable() ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $expected = [ 'before' => [ @@ -663,8 +674,8 @@ public function testAddFilter() 'after' => [], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $filters = $filters->addFilter('Some\Class', 'some_alias'); @@ -679,9 +690,9 @@ public function testAddFilterSection() { $_SERVER['REQUEST_METHOD'] = 'GET'; - $config = []; - - $filters = new Filters((object) $config, $this->request, $this->response); + $config = []; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $filters = $filters ->addFilter('Some\OtherClass', 'another', 'before', 'globals') @@ -696,9 +707,9 @@ public function testInitializeTwice() { $_SERVER['REQUEST_METHOD'] = 'GET'; - $config = []; - - $filters = new Filters((object) $config, $this->request, $this->response); + $config = []; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $filters = $filters ->addFilter('Some\OtherClass', 'another', 'before', 'globals') @@ -721,8 +732,8 @@ public function testEnableFilter() 'after' => [], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $filters = $filters->initialize('admin/foo/bar'); @@ -744,9 +755,9 @@ public function testEnableFilterWithArguments() 'after' => [], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); - $filters = $filters->initialize('admin/foo/bar'); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $filters = $filters->initialize('admin/foo/bar'); $filters->enableFilter('role:admin , super', 'before'); $filters->enableFilter('role:admin , super', 'after'); @@ -775,8 +786,8 @@ public function testEnableFilterWithNoArguments() 'after' => [], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $filters = $filters->initialize('admin/foo/bar'); @@ -807,8 +818,8 @@ public function testEnableNonFilter() 'after' => [], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $filters = $filters->initialize('admin/foo/bar'); @@ -846,8 +857,9 @@ public function testMatchesURICaseInsensitively() ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $expected = [ 'before' => [ @@ -883,9 +895,9 @@ public function testFilterMatching() ], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin'; $expected = [ 'before' => [ @@ -922,9 +934,9 @@ public function testGlobalFilterMatching() ], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin'; $expected = [ 'before' => [ @@ -971,9 +983,9 @@ public function testCombinedFilterMatching() ], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin123'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin123'; $expected = [ 'before' => [ @@ -1017,9 +1029,9 @@ public function testSegmentedFilterMatching() ], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/123'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/123'; $expected = [ 'before' => [ @@ -1051,8 +1063,9 @@ public function testFilterAlitasMultiple() ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin/foo/bar'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; $request = $filters->run($uri, 'before'); $this->assertSame('http://exampleMultipleURL.com', $request->url); @@ -1074,7 +1087,8 @@ public function testFilterClass() ], ], ]; - $filters = new Filters((object) $config, $this->request, $this->response); + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); $filters->run('admin/foo/bar', 'before'); $expected = [ @@ -1101,9 +1115,9 @@ public function testReset() ], ], ]; - - $filters = new Filters((object) $config, $this->request, $this->response); - $uri = 'admin'; + $this->request = Services::request(); + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin'; $this->assertSame(['foo'], $filters->initialize($uri)->getFilters()['before']); $this->assertSame([], $filters->reset()->getFilters()['before']); From 11b5c265ae8e121fe8a2d6326ec742103422d8a6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 21 Oct 2021 19:02:26 +0900 Subject: [PATCH 351/490] test: fix tests Security object should be created after setting super globals. --- tests/system/Security/SecurityTest.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/system/Security/SecurityTest.php b/tests/system/Security/SecurityTest.php index eadcf5ff6326..9f14e355b981 100644 --- a/tests/system/Security/SecurityTest.php +++ b/tests/system/Security/SecurityTest.php @@ -79,13 +79,13 @@ public function testGetHashSetsCookieWhenNotPOST() public function testCSRFVerifyPostThrowsExceptionOnNoMatch() { - $security = new MockSecurity(new MockAppConfig()); - $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $_SERVER['REQUEST_METHOD'] = 'POST'; $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; + $security = new MockSecurity(new MockAppConfig()); + $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); + $this->expectException(SecurityException::class); $security->verify($request); } @@ -108,14 +108,14 @@ public function testCSRFVerifyPostReturnsSelfOnMatch() public function testCSRFVerifyHeaderThrowsExceptionOnNoMatch() { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; + $security = new MockSecurity(new MockAppConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; - $this->expectException(SecurityException::class); $security->verify($request); } @@ -139,14 +139,14 @@ public function testCSRFVerifyHeaderReturnsSelfOnMatch() public function testCSRFVerifyJsonThrowsExceptionOnNoMatch() { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; + $security = new MockSecurity(new MockAppConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setBody('{"csrf_test_name":"8b9218a55906f9dcc1dc263dce7f005a"}'); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; - $this->expectException(SecurityException::class); $security->verify($request); } From 070cdb458dbbd771a2a9175f04f97c64a0ae9cb3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 22 Oct 2021 11:25:33 +0900 Subject: [PATCH 352/490] docs: update user guide --- user_guide_src/source/changelogs/v4.1.5.rst | 6 ++++++ .../source/installation/upgrade_415.rst | 17 +++++++++++++++++ user_guide_src/source/libraries/security.rst | 10 ++++++++++ 3 files changed, 33 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.1.5.rst b/user_guide_src/source/changelogs/v4.1.5.rst index 328e45163cb3..3290763f0ed3 100644 --- a/user_guide_src/source/changelogs/v4.1.5.rst +++ b/user_guide_src/source/changelogs/v4.1.5.rst @@ -5,6 +5,12 @@ Release Date: Not released **4.1.5 release of CodeIgniter4** +BREAKING: + +Fixed `a bug `_ on CSRF protection. +Now CSRF protection works on PUT/PATCH/DELETE requests when CSRF filter is applied. +If you use such requests, you need to send CSRF token. + Enhancements: - Added Cache config for reserved characters diff --git a/user_guide_src/source/installation/upgrade_415.rst b/user_guide_src/source/installation/upgrade_415.rst index b05d0d0030a6..c9a6eb07da6e 100644 --- a/user_guide_src/source/installation/upgrade_415.rst +++ b/user_guide_src/source/installation/upgrade_415.rst @@ -53,3 +53,20 @@ The following methods and a property have been deprecated: - ``CodeIgniter\Router\RouteCollection``'s property ``$filterInfo`` See *Applying Filters* in :doc:`Routing ` for the functionality. + +**CSRF Protection** + +Because of a bug fix, +now CSRF protection works on not only **POST** but also **PUT/PATCH/DELETE** requests when CSRF filter is applied. + +When you use **PUT/PATCH/DELETE** requests, you need to send CSRF token. Or remove the CSRF filter +for such requests if you don't need CSRF protection for them. + +If you want the same behavior as the previous version, set the CSRF filter like the following in **app/Config/Filters.php**:: + + public $methods = [ + 'get' => ['csrf'], + 'post' => ['csrf'], + ]; + +Protecting **GET** method needs only when you use ``form_open()`` auto-generation of CSRF field. diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index d788e2d2a808..48b97baf1f61 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -23,6 +23,9 @@ If you find a case where you do need direct access though, you may load it throu Cross-site request forgery (CSRF) ********************************* +.. warning:: The CSRF Protection is only available for **POST/PUT/PATCH/DELETE** requests. + Requests for other methods are not protected. + Enable CSRF Protection ====================== @@ -54,6 +57,13 @@ Regular expressions are also supported (case-insensitive):: ], ]; +It is also possible to enable the CSRF filter only for specific methods:: + + public $methods = [ + 'get' => ['csrf'], + 'post' => ['csrf'], + ]; + HTML Forms ========== From e19228acfbaac655256498ef746d16e47247bc6c Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 22 Oct 2021 10:38:44 +0900 Subject: [PATCH 353/490] docs: improve Upgrading 4.1.5 page format --- .../source/installation/upgrade_415.rst | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_415.rst b/user_guide_src/source/installation/upgrade_415.rst index c9a6eb07da6e..6c20797775af 100644 --- a/user_guide_src/source/installation/upgrade_415.rst +++ b/user_guide_src/source/installation/upgrade_415.rst @@ -2,7 +2,15 @@ Upgrading from 4.1.4 to 4.1.5 ############################# -**Changes for set() method in BaseBuilder and Model class** +.. contents:: + :local: + :depth: 1 + +Breaking Changes +================ + +Changes for set() method in BaseBuilder and Model class +------------------------------------------------------- The casting for the ``$value`` parameter has been removed to fix a bug where passing parameters as array and string to the ``set()`` method were handled differently. If you extended the ``BaseBuilder`` class or ``Model`` class yourself @@ -10,7 +18,8 @@ and modified the ``set()`` method, then you need to change its definition from ``public function set($key, ?string $value = '', ?bool $escape = null)`` to ``public function set($key, $value = '', ?bool $escape = null)``. -**Session DatabaseHandler's database table change** +Session DatabaseHandler's database table change +----------------------------------------------- The types of the following columns in the session table have been changed for optimization. @@ -26,7 +35,29 @@ Update the definition of the session table. See the :doc:`/libraries/sessions` f The change was introduced in v4.1.2. But due to `a bug `_, the DatabaseHandler Driver did not work properly. -**Multiple filters for a route** +CSRF Protection +--------------- + +Because of a bug fix, +now CSRF protection works on not only **POST** but also **PUT/PATCH/DELETE** requests when CSRF filter is applied. + +When you use **PUT/PATCH/DELETE** requests, you need to send CSRF token. Or remove the CSRF filter +for such requests if you don't need CSRF protection for them. + +If you want the same behavior as the previous version, set the CSRF filter like the following in **app/Config/Filters.php**:: + + public $methods = [ + 'get' => ['csrf'], + 'post' => ['csrf'], + ]; + +Protecting **GET** method needs only when you use ``form_open()`` auto-generation of CSRF field. + +Breaking Enhancements +===================== + +Multiple filters for a route +---------------------------- A new feature to set multiple filters for a route. @@ -54,19 +85,11 @@ The following methods and a property have been deprecated: See *Applying Filters* in :doc:`Routing ` for the functionality. -**CSRF Protection** - -Because of a bug fix, -now CSRF protection works on not only **POST** but also **PUT/PATCH/DELETE** requests when CSRF filter is applied. - -When you use **PUT/PATCH/DELETE** requests, you need to send CSRF token. Or remove the CSRF filter -for such requests if you don't need CSRF protection for them. - -If you want the same behavior as the previous version, set the CSRF filter like the following in **app/Config/Filters.php**:: +Project Files +============= - public $methods = [ - 'get' => ['csrf'], - 'post' => ['csrf'], - ]; +Content Changes +--------------- -Protecting **GET** method needs only when you use ``form_open()`` auto-generation of CSRF field. +All Changes +----------- From f98556127023f89354b3ec2276cd4757beadd577 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" <51850998+paulbalandan@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:18:49 +0800 Subject: [PATCH 354/490] Rename toolbar loader to be a regular JS file * Rename toolbar loader to be a regular JS file * Use `str_replace` instead of `window.appUrl` --- system/Debug/Toolbar.php | 4 ++-- .../Views/{toolbarloader.js.php => toolbarloader.js} | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) rename system/Debug/Toolbar/Views/{toolbarloader.js.php => toolbarloader.js} (96%) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 2199559d362d..b951e2ed9658 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -435,9 +435,9 @@ public function respond() header('Content-Type: application/javascript'); ob_start(); - include $this->config->viewsPath . 'toolbarloader.js.php'; + include $this->config->viewsPath . 'toolbarloader.js'; $output = ob_get_clean(); - $output = substr($output, 8, -10); // trim the script tags + $output = str_replace('{url}', rtrim(site_url(), '/'), $output); echo $output; exit; diff --git a/system/Debug/Toolbar/Views/toolbarloader.js.php b/system/Debug/Toolbar/Views/toolbarloader.js similarity index 96% rename from system/Debug/Toolbar/Views/toolbarloader.js.php rename to system/Debug/Toolbar/Views/toolbarloader.js index add85b52acda..7e5914354481 100644 --- a/system/Debug/Toolbar/Views/toolbarloader.js.php +++ b/system/Debug/Toolbar/Views/toolbarloader.js @@ -1,7 +1,3 @@ - - From 4876b4855e1568e3aad66c1c89525034f0996986 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sun, 24 Oct 2021 15:58:34 +0700 Subject: [PATCH 355/490] [HTTP] Update Http Status Description based on latest iana.org --- system/HTTP/Response.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index 70f31bd35f72..4efc6e804d3f 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -79,15 +79,15 @@ class Response extends Message implements MessageInterface, ResponseInterface 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Long', + 413 => 'Content Too Large', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml + 414 => 'URI Too Long', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', 418 => "I'm a teapot", // April's Fools joke; http://www.ietf.org/rfc/rfc2324.txt // 419 (Authentication Timeout) is a non-standard status code with unknown origin 421 => 'Misdirected Request', // http://www.iana.org/go/rfc7540 Section 9.1.2 - 422 => 'Unprocessable Entity', // http://www.iana.org/go/rfc4918 + 422 => 'Unprocessable Content', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml 423 => 'Locked', // http://www.iana.org/go/rfc4918 424 => 'Failed Dependency', // http://www.iana.org/go/rfc4918 425 => 'Too Early', // https://datatracker.ietf.org/doc/draft-ietf-httpbis-replay/ From 1121abae4693ad3d36a85bc4a5edcce23709d43f Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 25 Oct 2021 10:39:05 +0900 Subject: [PATCH 356/490] test: reduce the size of the method to be mocked --- system/Test/Mock/MockSecurity.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/system/Test/Mock/MockSecurity.php b/system/Test/Mock/MockSecurity.php index 3f802a7d79f6..e24221c17b47 100644 --- a/system/Test/Mock/MockSecurity.php +++ b/system/Test/Mock/MockSecurity.php @@ -11,15 +11,12 @@ namespace CodeIgniter\Test\Mock; -use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\Security\Security; class MockSecurity extends Security { - protected function sendCookie(RequestInterface $request) + protected function doSendCookie(): void { $_COOKIE['csrf_cookie_name'] = $this->hash; - - return $this; } } From 6af9fc56ba06b42efe366bbde198c80cf5d6fabf Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 25 Oct 2021 10:41:16 +0900 Subject: [PATCH 357/490] test: add a test case --- tests/system/Security/SecurityTest.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/system/Security/SecurityTest.php b/tests/system/Security/SecurityTest.php index 9f14e355b981..ba6a4ada5024 100644 --- a/tests/system/Security/SecurityTest.php +++ b/tests/system/Security/SecurityTest.php @@ -66,7 +66,7 @@ public function testHashIsReadFromCookie() $this->assertSame('8b9218a55906f9dcc1dc263dce7f005a', $security->getHash()); } - public function testGetHashSetsCookieWhenNotPOST() + public function testGetHashSetsCookieWhenGETWithoutCSRFCookie() { $security = new MockSecurity(new MockAppConfig()); @@ -77,6 +77,18 @@ public function testGetHashSetsCookieWhenNotPOST() $this->assertSame($_COOKIE['csrf_cookie_name'], $security->getHash()); } + public function testGetHashReturnsCSRFCookieWhenGETWithCSRFCookie() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + + $security = new MockSecurity(new MockAppConfig()); + + $security->verify(new Request(new MockAppConfig())); + + $this->assertSame($_COOKIE['csrf_cookie_name'], $security->getHash()); + } + public function testCSRFVerifyPostThrowsExceptionOnNoMatch() { $_SERVER['REQUEST_METHOD'] = 'POST'; From 2a3767ad91c5387e67307d815264a99da8d18dc9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 25 Oct 2021 11:09:13 +0900 Subject: [PATCH 358/490] docs: fix format of testing/overview.rst --- user_guide_src/source/testing/overview.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/testing/overview.rst b/user_guide_src/source/testing/overview.rst index f237acc03412..0a16a07b34ac 100644 --- a/user_guide_src/source/testing/overview.rst +++ b/user_guide_src/source/testing/overview.rst @@ -80,7 +80,7 @@ To test a new library, **Foo**, you would create a new file at **tests/app/Libra } } -To test one of your models, you might end up with something like this in ``tests/app/Models/OneOfMyModelsTest.php``:: +To test one of your models, you might end up with something like this in **tests/app/Models/OneOfMyModelsTest.php**:: assertEventTriggered('foo'); -**assertHeaderEmitted($header, $ignoreCase=false)** +**assertHeaderEmitted($header, $ignoreCase = false)** Ensure that a header or cookie was actually emitted:: @@ -218,7 +218,7 @@ Ensure that a header or cookie was actually emitted:: Note: the test case with this should be `run as a separate process in PHPunit `_. -**assertHeaderNotEmitted($header, $ignoreCase=false)** +**assertHeaderNotEmitted($header, $ignoreCase = false)** Ensure that a header or cookie was not emitted:: @@ -233,7 +233,7 @@ Ensure that a header or cookie was not emitted:: Note: the test case with this should be `run as a separate process in PHPunit `_. -**assertCloseEnough($expected, $actual, $message='', $tolerance=1)** +**assertCloseEnough($expected, $actual, $message = '', $tolerance = 1)** For extended execution time testing, tests that the absolute difference between expected and actual time is within the prescribed tolerance.:: @@ -244,7 +244,7 @@ between expected and actual time is within the prescribed tolerance.:: The above test will allow the actual time to be either 660 or 661 seconds. -**assertCloseEnoughString($expected, $actual, $message='', $tolerance=1)** +**assertCloseEnoughString($expected, $actual, $message = '', $tolerance = 1)** For extended execution time testing, tests that the absolute difference between expected and actual time, formatted as strings, is within the prescribed tolerance.:: From e2f4e59e253363b6a191a80e9a208a09f1371997 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 25 Oct 2021 11:14:07 +0900 Subject: [PATCH 359/490] docs: Zip file does not contain the user guide --- user_guide_src/source/installation/upgrade_4xx.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_4xx.rst b/user_guide_src/source/installation/upgrade_4xx.rst index bfc38a852778..f28de5092e3d 100644 --- a/user_guide_src/source/installation/upgrade_4xx.rst +++ b/user_guide_src/source/installation/upgrade_4xx.rst @@ -30,7 +30,7 @@ General Adjustments **Downloads** -- CI4 is still available as a ready-to-run zip or tarball, which includes the user guide (though in the `docs` subfolder). +- CI4 is still available as a ready-to-run zip or tarball. - It can also be installed using Composer. **Namespaces** From 024df6cbedfacfe509999abf85bdaa0ef957dfcc Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 25 Oct 2021 11:32:34 +0900 Subject: [PATCH 360/490] fix: remove extra DIRECTORY_SEPARATOR Co-authored-by: MGatner --- system/Publisher/Publisher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index a492a03e02c3..dac7aa898f1a 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -230,7 +230,7 @@ final public function getScratch(): string $this->scratch = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)) . DIRECTORY_SEPARATOR; mkdir($this->scratch, 0700); $this->scratch = realpath($this->scratch) ? realpath($this->scratch) . DIRECTORY_SEPARATOR - : $this->scratch . DIRECTORY_SEPARATOR; + : $this->scratch; } return $this->scratch; From 7634d3a7a7f9f3bd8848e956a5797f9c40e78f02 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 25 Oct 2021 14:35:29 +0700 Subject: [PATCH 361/490] [Rector] Using LevelSetList::UP_TO_PHP_73 --- rector.php | 20 +++++++++++++++++++- tests/system/Autoloader/FileLocatorTest.php | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/rector.php b/rector.php index aa9546d37ca9..fdd78a85e86c 100644 --- a/rector.php +++ b/rector.php @@ -37,11 +37,16 @@ use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector; use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector; use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector; +use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector; +use Rector\Php56\Rector\FunctionLike\AddDefaultValueForUndefinedVariableRector; +use Rector\Php70\Rector\FuncCall\RandomFunctionRector; use Rector\Php70\Rector\Ternary\TernaryToNullCoalescingRector; +use Rector\Php71\Rector\FuncCall\CountOnNullRector; use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Php71\Rector\List_\ListToArrayDestructRector; use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector; use Rector\Php73\Rector\FuncCall\StringifyStrNeedlesRector; +use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\SetList; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Utils\Rector\PassStrictParameterToFunctionParameterRector; @@ -51,7 +56,7 @@ return static function (ContainerConfigurator $containerConfigurator): void { $containerConfigurator->import(SetList::DEAD_CODE); - $containerConfigurator->import(SetList::PHP_73); + $containerConfigurator->import(LevelSetList::UP_TO_PHP_73); $parameters = $containerConfigurator->parameters(); @@ -95,6 +100,19 @@ UnderscoreToCamelCaseVariableNameRector::class => [ __DIR__ . '/system/Session/Handlers', ], + + // may cause load view files directly when detecting class that + // make warning + StringClassNameToClassConstantRector::class, + + // sometime too detail + CountOnNullRector::class, + + // may not be unitialized on purpose + AddDefaultValueForUndefinedVariableRector::class, + + // use mt_rand instead of random_int on purpose on non-cryptographically random + RandomFunctionRector::class, ]); // auto import fully qualified class names diff --git a/tests/system/Autoloader/FileLocatorTest.php b/tests/system/Autoloader/FileLocatorTest.php index 8a8b91aeea90..c85e4c215ba2 100644 --- a/tests/system/Autoloader/FileLocatorTest.php +++ b/tests/system/Autoloader/FileLocatorTest.php @@ -261,7 +261,7 @@ public function testFindQNameFromPathWithoutCorrespondingNamespace() public function testGetClassNameFromClassFile() { $this->assertSame( - __CLASS__, + self::class, $this->locator->getClassname(__FILE__) ); } From dd72eef46b63a7fb87ef38e0a0be971de8d68bf6 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 25 Oct 2021 14:42:42 +0700 Subject: [PATCH 362/490] clean up existing already in set list --- rector.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/rector.php b/rector.php index fdd78a85e86c..04e761cbf89e 100644 --- a/rector.php +++ b/rector.php @@ -40,10 +40,7 @@ use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector; use Rector\Php56\Rector\FunctionLike\AddDefaultValueForUndefinedVariableRector; use Rector\Php70\Rector\FuncCall\RandomFunctionRector; -use Rector\Php70\Rector\Ternary\TernaryToNullCoalescingRector; use Rector\Php71\Rector\FuncCall\CountOnNullRector; -use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; -use Rector\Php71\Rector\List_\ListToArrayDestructRector; use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector; use Rector\Php73\Rector\FuncCall\StringifyStrNeedlesRector; use Rector\Set\ValueObject\LevelSetList; @@ -141,12 +138,9 @@ $services->set(ChangeArrayPushToArrayAssignRector::class); $services->set(UnnecessaryTernaryExpressionRector::class); $services->set(RemoveErrorSuppressInTryCatchStmtsRector::class); - $services->set(TernaryToNullCoalescingRector::class); - $services->set(ListToArrayDestructRector::class); $services->set(RemoveVarTagFromClassConstantRector::class); $services->set(AddPregQuoteDelimiterRector::class); $services->set(SimplifyRegexPatternRector::class); - $services->set(RemoveExtraParametersRector::class); $services->set(FuncGetArgsToVariadicParamRector::class); $services->set(MakeInheritedMethodVisibilitySameAsParentRector::class); $services->set(FixClassCaseSensitivityNameRector::class); From d1edaee437d256d9cc1e16c76c4fec2ba8de48c0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 19 Oct 2021 21:57:21 +0900 Subject: [PATCH 363/490] fix: reset unneeded headers to prevent request error Fixes #4826 --- system/HTTP/CURLRequest.php | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 3be8214d42b4..3b4b3ef9a850 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -72,6 +72,16 @@ class CURLRequest extends Request */ protected $delay = 0.0; + /** + * Request headers that are not shared between requests. + * + * @var string[] + */ + private $unsharedHeaders = [ + 'Content-Length', + 'Content-Type', + ]; + /** * Takes an array of options to set the following possible class properties: * @@ -106,6 +116,11 @@ public function __construct(App $config, URI $uri, ?ResponseInterface $response */ public function request($method, string $url, array $options = []): ResponseInterface { + // Reset unshared headers + foreach ($this->unsharedHeaders as $header) { + $this->removeHeader($header); + } + $this->parseOptions($options); $url = $this->prepareURL($url); @@ -379,6 +394,29 @@ protected function applyRequestHeaders(array $curlOptions = []): array return $curlOptions; } + /** + * Override + */ + public function populateHeaders(): void + { + foreach (array_keys($_SERVER) as $key) { + if (sscanf($key, 'HTTP_%s', $header) === 1) { + // take SOME_HEADER and turn it into Some-Header + $header = str_replace('_', ' ', strtolower($header)); + $header = str_replace(' ', '-', ucwords($header)); + + if (in_array($header, $this->unsharedHeaders, true)) { + continue; + } + + $this->setHeader($header, $_SERVER[$key]); + + // Add us to the header map so we can find them case-insensitively + $this->headerMap[strtolower($header)] = $header; + } + } + } + /** * Apply method */ From 245dade5a52217c9eff313e6caa76c4c8531490e Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 19 Oct 2021 21:58:32 +0900 Subject: [PATCH 364/490] fix: reset unneeded configs If you reset it at the beginning of the request(), you will not be able to use setForm(). --- system/HTTP/CURLRequest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 3b4b3ef9a850..2a305fa1c802 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -129,6 +129,9 @@ public function request($method, string $url, array $options = []): ResponseInte $this->send($method, $url); + // Reset unshared configs + unset($this->config['multipart'], $this->config['form_params']); + return $this->response; } From fa3620b24ae7bb15c00ca4fb49e56d36a8f4c510 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 19 Oct 2021 22:03:44 +0900 Subject: [PATCH 365/490] test: add test for CURLRequest unshared headers --- tests/system/HTTP/CURLRequestTest.php | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index c1ac77765a44..56420caa863f 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -220,6 +220,46 @@ public function testOptionHeadersNotUsingPopulate() $this->assertSame('', $request->header('Accept-Encoding')->getValue()); } + public function testHeaderContentLengthNotSharedBetweenRequests() + { + $options = [ + 'base_uri' => 'http://www.foo.com/api/v1/', + ]; + $request = $this->getRequest($options); + + $request->post('example', [ + 'form_params' => [ + 'q' => 'keyword', + ], + ]); + $request->get('example'); + + $this->assertNull($request->header('Content-Length')); + } + + /** + * @backupGlobals enabled + */ + public function testHeaderContentLengthNotSharedBetweenClients() + { + $_SERVER['HTTP_CONTENT_LENGTH'] = '10'; + + $options = [ + 'base_uri' => 'http://www.foo.com/api/v1/', + ]; + $request = $this->getRequest($options); + $request->post('example', [ + 'form_params' => [ + 'q' => 'keyword', + ], + ]); + + $request = $this->getRequest($options); + $request->get('example'); + + $this->assertNull($request->header('Content-Length')); + } + public function testOptionsDelay() { $options = [ From 4b51e01b5b039d277b41930482220521f7af9970 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 19 Oct 2021 22:03:59 +0900 Subject: [PATCH 366/490] test: fix test method names --- tests/system/HTTP/CURLRequestTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index 56420caa863f..b46cec5ce992 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -176,7 +176,7 @@ public function testOptionsHeaders() /** * @backupGlobals enabled */ - public function testOptionHeadersUsingPopulate() + public function testOptionsHeadersUsingPopulate() { $_SERVER['HTTP_HOST'] = 'site1.com'; $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en-US'; @@ -199,7 +199,7 @@ public function testOptionHeadersUsingPopulate() /** * @backupGlobals enabled */ - public function testOptionHeadersNotUsingPopulate() + public function testOptionsHeadersNotUsingPopulate() { $_SERVER['HTTP_HOST'] = 'site1.com'; $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en-US'; From c497b28998eff9a0c7786237b53f7391bb741c50 Mon Sep 17 00:00:00 2001 From: MGatner Date: Mon, 25 Oct 2021 11:21:40 -0400 Subject: [PATCH 367/490] Switch PHPCPD to tool --- .github/workflows/test-phpcpd.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-phpcpd.yml b/.github/workflows/test-phpcpd.yml index d78edbea1f0e..f4215df01c0e 100644 --- a/.github/workflows/test-phpcpd.yml +++ b/.github/workflows/test-phpcpd.yml @@ -34,10 +34,8 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.0' - tools: phive - extensions: intl, json, mbstring, xml + tools: phpcpd + extensions: dom, mbstring - name: Detect code duplication - run: | - sudo phive --no-progress install --global --trust-gpg-keys 4AA394086372C20A phpcpd - phpcpd --exclude system/Test --exclude system/ThirdParty --exclude system/Database/SQLSRV/Builder.php --exclude system/Database/SQLSRV/Forge.php app/ public/ system/ + run: phpcpd --exclude system/Test --exclude system/ThirdParty --exclude system/Database/SQLSRV/Builder.php --exclude system/Database/SQLSRV/Forge.php -- app/ public/ system/ From 0061d9d80cb404796e67ed25a7de55a695097a41 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 26 Oct 2021 12:56:03 +0900 Subject: [PATCH 368/490] docs: remove unnecessary headings --- user_guide_src/source/libraries/typography.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/user_guide_src/source/libraries/typography.rst b/user_guide_src/source/libraries/typography.rst index 24e52419df35..9b381a151cdc 100644 --- a/user_guide_src/source/libraries/typography.rst +++ b/user_guide_src/source/libraries/typography.rst @@ -24,8 +24,6 @@ Available static functions The following functions are available: -**autoTypography()** - .. php:function:: autoTypography($str[, $reduce_linebreaks = false]) :param string $str: Input string @@ -45,8 +43,6 @@ The following functions are available: function you may want to consider :doc:`caching <../general/caching>` your pages. -**formatCharacters()** - .. php:function:: formatCharacters($str) :param string $str: Input string @@ -61,8 +57,6 @@ The following functions are available: $string = $typography->formatCharacters($string); -**nl2brExceptPre()** - .. php:function:: nl2brExceptPre($str) :param string $str: Input string From 5a1c7808c94b73f096c606672d639545dee3f32e Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 26 Oct 2021 12:56:57 +0900 Subject: [PATCH 369/490] docs: decorate HTML tags --- user_guide_src/source/libraries/typography.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/libraries/typography.rst b/user_guide_src/source/libraries/typography.rst index 9b381a151cdc..0a4ded63a898 100644 --- a/user_guide_src/source/libraries/typography.rst +++ b/user_guide_src/source/libraries/typography.rst @@ -63,9 +63,9 @@ The following functions are available: :returns: String with HTML-formatted line breaks :rtype: string - Converts newlines to
    tags unless they appear within
     tags.
    +    Converts newlines to ``
    `` tags unless they appear within ``
    `` tags.
         This function is identical to the native PHP ``nl2br()`` function,
    -    except that it ignores 
     tags.
    +    except that it ignores ``
    `` tags.
     
         Usage example::
     
    
    From 4de18315b909eb89e38c5539b8c157e2f7f883e0 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Wed, 27 Oct 2021 08:38:40 +0900
    Subject: [PATCH 370/490] test: fix failed test
    
    1) CodeIgniter\Commands\CreateDatabaseTest::testCreateDatabase
    Failed asserting that 'Database ".../CodeIgniter4/writable/foobar.db" already exists.\n
    \n
    ' contains "successfully created.".
    
    .../CodeIgniter4/tests/system/Commands/CreateDatabaseTest.php:68
    ---
     tests/system/Commands/CreateDatabaseTest.php | 15 +++++----------
     1 file changed, 5 insertions(+), 10 deletions(-)
    
    diff --git a/tests/system/Commands/CreateDatabaseTest.php b/tests/system/Commands/CreateDatabaseTest.php
    index aed4fd5c144e..3875a2f51e68 100644
    --- a/tests/system/Commands/CreateDatabaseTest.php
    +++ b/tests/system/Commands/CreateDatabaseTest.php
    @@ -38,22 +38,17 @@ protected function setUp(): void
             $this->connection   = Database::connect();
     
             parent::setUp();
    +
    +        $file = WRITEPATH . 'foobar.db';
    +        if (file_exists($file)) {
    +            unlink($file);
    +        }
         }
     
         protected function tearDown(): void
         {
             stream_filter_remove($this->streamFilter);
     
    -        if ($this->connection instanceof Connection) {
    -            $file = WRITEPATH . 'foobar.db';
    -
    -            if (file_exists($file)) {
    -                unlink($file);
    -            }
    -        } else {
    -            Database::forge()->dropDatabase('foobar');
    -        }
    -
             parent::tearDown();
         }
     
    
    From 0923134ec7b31604208e192f4d209d739096029f Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Wed, 27 Oct 2021 08:42:45 +0900
    Subject: [PATCH 371/490] test: fix failed test
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    2) CodeIgniter\I18n\TimeTest::testToFormattedDateString
    Failed asserting that two strings are identical.
    --- Expected
    +++ Actual
    @@ @@
    -'May 10, 2017'
    +'5月 10, 2017'
    
    .../CodeIgniter4/tests/system/I18n/TimeTest.php:643
    ---
     tests/system/I18n/TimeTest.php | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php
    index 2da8e5accbf3..da1ef5b3c5ef 100644
    --- a/tests/system/I18n/TimeTest.php
    +++ b/tests/system/I18n/TimeTest.php
    @@ -28,7 +28,7 @@ protected function setUp(): void
             parent::setUp();
     
             helper('date');
    -        Locale::setDefault('America/Chicago');
    +        Locale::setDefault('en_US');
         }
     
         public function testNewTimeNow()
    
    From f1a9f3bb1985350ed527a02e514b980dffc4b5d7 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Wed, 27 Oct 2021 11:28:12 +0900
    Subject: [PATCH 372/490] test: fix setUp()
    
    Drop database for non-SQLite connections.
    ---
     tests/system/Commands/CreateDatabaseTest.php | 16 ++++++++++------
     1 file changed, 10 insertions(+), 6 deletions(-)
    
    diff --git a/tests/system/Commands/CreateDatabaseTest.php b/tests/system/Commands/CreateDatabaseTest.php
    index 3875a2f51e68..1b6d28fdf0f8 100644
    --- a/tests/system/Commands/CreateDatabaseTest.php
    +++ b/tests/system/Commands/CreateDatabaseTest.php
    @@ -12,7 +12,7 @@
     namespace CodeIgniter\Commands;
     
     use CodeIgniter\Database\BaseConnection;
    -use CodeIgniter\Database\SQLite3\Connection;
    +use CodeIgniter\Database\SQLite3\Connection as SQLite3Connection;
     use CodeIgniter\Test\CIUnitTestCase;
     use CodeIgniter\Test\Filters\CITestStreamFilter;
     use Config\Database;
    @@ -39,9 +39,13 @@ protected function setUp(): void
     
             parent::setUp();
     
    -        $file = WRITEPATH . 'foobar.db';
    -        if (file_exists($file)) {
    -            unlink($file);
    +        if ($this->connection instanceof SQLite3Connection) {
    +            $file = WRITEPATH . 'foobar.db';
    +            if (file_exists($file)) {
    +                unlink($file);
    +            }
    +        } else {
    +            Database::forge()->dropDatabase('foobar');
             }
         }
     
    @@ -65,7 +69,7 @@ public function testCreateDatabase()
     
         public function testSqliteDatabaseDuplicated()
         {
    -        if (! $this->connection instanceof Connection) {
    +        if (! $this->connection instanceof SQLite3Connection) {
                 $this->markTestSkipped('Needs to run on SQLite3.');
             }
     
    @@ -78,7 +82,7 @@ public function testSqliteDatabaseDuplicated()
     
         public function testOtherDriverDuplicatedDatabase()
         {
    -        if ($this->connection instanceof Connection) {
    +        if ($this->connection instanceof SQLite3Connection) {
                 $this->markTestSkipped('Needs to run on non-SQLite3 drivers.');
             }
     
    
    From ee2b89481b7255bc64e0a409a164bdb7ba252755 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA" 
    Date: Wed, 27 Oct 2021 11:33:58 +0800
    Subject: [PATCH 373/490] Fix wrong helper path resolution
    
    ---
     system/Common.php                    |  16 +--
     tests/system/CommonFunctionsTest.php |  39 -------
     tests/system/CommonHelperTest.php    | 148 +++++++++++++++++++++++++++
     3 files changed, 153 insertions(+), 50 deletions(-)
     create mode 100644 tests/system/CommonHelperTest.php
    
    diff --git a/system/Common.php b/system/Common.php
    index 0750fe38764e..7c150a01c1d4 100644
    --- a/system/Common.php
    +++ b/system/Common.php
    @@ -560,7 +560,7 @@ function helper($filenames)
         {
             static $loaded = [];
     
    -        $loader = Services::locator(true);
    +        $loader = Services::locator();
     
             if (! is_array($filenames)) {
                 $filenames = [$filenames];
    @@ -596,18 +596,14 @@ function helper($filenames)
     
                     $includes[] = $path;
                     $loaded[]   = $filename;
    -            }
    -
    -            // No namespaces, so search in all available locations
    -            else {
    +            } else {
    +                // No namespaces, so search in all available locations
                     $paths = $loader->search('Helpers/' . $filename);
     
                     foreach ($paths as $path) {
    -                    if (strpos($path, APPPATH) === 0) {
    -                        // @codeCoverageIgnoreStart
    +                    if (strpos($path, APPPATH . 'Helpers' . DIRECTORY_SEPARATOR) === 0) {
                             $appHelper = $path;
    -                    // @codeCoverageIgnoreEnd
    -                    } elseif (strpos($path, SYSTEMPATH) === 0) {
    +                    } elseif (strpos($path, SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR) === 0) {
                             $systemHelper = $path;
                         } else {
                             $localIncludes[] = $path;
    @@ -617,10 +613,8 @@ function helper($filenames)
     
                     // App-level helpers should override all others
                     if (! empty($appHelper)) {
    -                    // @codeCoverageIgnoreStart
                         $includes[] = $appHelper;
                         $loaded[]   = $filename;
    -                    // @codeCoverageIgnoreEnd
                     }
     
                     // All namespaced files get added in next
    diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php
    index ad5ae969b1bc..ba4c1e7a4e07 100644
    --- a/tests/system/CommonFunctionsTest.php
    +++ b/tests/system/CommonFunctionsTest.php
    @@ -29,9 +29,7 @@
     use Config\Logger;
     use Config\Modules;
     use InvalidArgumentException;
    -use RuntimeException;
     use stdClass;
    -use Tests\Support\Autoloader\FatalLocator;
     use Tests\Support\Models\JobModel;
     
     /**
    @@ -479,43 +477,6 @@ public function dirtyPathsProvider()
             ];
         }
     
    -    public function testHelperWithFatalLocatorThrowsException()
    -    {
    -        // Replace the locator with one that will fail if it is called
    -        $locator = new FatalLocator(Services::autoloader());
    -        Services::injectMock('locator', $locator);
    -
    -        try {
    -            helper('baguette');
    -            $exception = false;
    -        } catch (RuntimeException $e) {
    -            $exception = true;
    -        }
    -
    -        $this->assertTrue($exception);
    -        Services::reset();
    -    }
    -
    -    public function testHelperLoadsOnce()
    -    {
    -        // Load it the first time
    -        helper('baguette');
    -
    -        // Replace the locator with one that will fail if it is called
    -        $locator = new FatalLocator(Services::autoloader());
    -        Services::injectMock('locator', $locator);
    -
    -        try {
    -            helper('baguette');
    -            $exception = false;
    -        } catch (RuntimeException $e) {
    -            $exception = true;
    -        }
    -
    -        $this->assertFalse($exception);
    -        Services::reset();
    -    }
    -
         public function testIsCli()
         {
             $this->assertIsBool(is_cli());
    diff --git a/tests/system/CommonHelperTest.php b/tests/system/CommonHelperTest.php
    new file mode 100644
    index 000000000000..55d626cf966c
    --- /dev/null
    +++ b/tests/system/CommonHelperTest.php
    @@ -0,0 +1,148 @@
    +
    + *
    + * For the full copyright and license information, please view
    + * the LICENSE file that was distributed with this source code.
    + */
    +
    +namespace CodeIgniter;
    +
    +use CodeIgniter\Autoloader\FileLocator;
    +use CodeIgniter\Test\CIUnitTestCase;
    +use Config\Services;
    +use PHPUnit\Framework\MockObject\MockObject;
    +use RuntimeException;
    +use Tests\Support\Autoloader\FatalLocator;
    +
    +/**
    + * @internal
    + *
    + * @covers ::helper
    + */
    +final class CommonHelperTest extends CIUnitTestCase
    +{
    +    private $dummyHelpers = [
    +        APPPATH . 'Helpers' . DIRECTORY_SEPARATOR . 'foobarbaz_helper.php',
    +        SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR . 'foobarbaz_helper.php',
    +    ];
    +
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +        $this->cleanUpDummyHelpers();
    +    }
    +
    +    protected function tearDown(): void
    +    {
    +        parent::tearDown();
    +        $this->cleanUpDummyHelpers();
    +        Services::reset();
    +    }
    +
    +    private function createDummyHelpers(): void
    +    {
    +        $text = <<<'PHP'
    +            dummyHelpers as $helper) {
    +            file_put_contents($helper, $text);
    +        }
    +    }
    +
    +    private function cleanUpDummyHelpers(): void
    +    {
    +        foreach ($this->dummyHelpers as $helper) {
    +            if (is_file($helper)) {
    +                unlink($helper);
    +            }
    +        }
    +    }
    +
    +    /**
    +     * @return FileLocator&MockObject
    +     */
    +    private function getMockLocator()
    +    {
    +        return $this->getMockBuilder(FileLocator::class)
    +            ->setConstructorArgs([Services::autoloader()])
    +            ->onlyMethods(['search'])
    +            ->getMock();
    +    }
    +
    +    public function testHelperWithFatalLocatorThrowsException()
    +    {
    +        // Replace the locator with one that will fail if it is called
    +        $locator = new FatalLocator(Services::autoloader());
    +        Services::injectMock('locator', $locator);
    +
    +        try {
    +            helper('baguette');
    +            $exception = false;
    +        } catch (RuntimeException $e) {
    +            $exception = true;
    +        }
    +
    +        $this->assertTrue($exception);
    +        Services::reset();
    +    }
    +
    +    public function testHelperLoadsOnce()
    +    {
    +        // Load it the first time
    +        helper('baguette');
    +
    +        // Replace the locator with one that will fail if it is called
    +        $locator = new FatalLocator(Services::autoloader());
    +        Services::injectMock('locator', $locator);
    +
    +        try {
    +            helper('baguette');
    +            $exception = false;
    +        } catch (RuntimeException $e) {
    +            $exception = true;
    +        }
    +
    +        $this->assertFalse($exception);
    +        Services::reset();
    +    }
    +
    +    public function testHelperLoadsAppHelperFirst(): void
    +    {
    +        foreach ($this->dummyHelpers as $helper) {
    +            $this->assertFileDoesNotExist($helper, sprintf(
    +                'The dummy helper file "%s" should not be existing before it is tested.',
    +                $helper
    +            ));
    +        }
    +
    +        $this->createDummyHelpers();
    +        $locator = $this->getMockLocator();
    +        $locator->method('search')->with('Helpers/foobarbaz_helper')->willReturn($this->dummyHelpers);
    +        Services::injectMock('locator', $locator);
    +
    +        helper('foobarbaz');
    +
    +        // this chunk is not needed really; just added so that IDEs will be happy
    +        if (! function_exists('foo_bar_baz')) {
    +            function foo_bar_baz(): string
    +            {
    +                return __FILE__;
    +            }
    +        }
    +
    +        $this->assertSame($this->dummyHelpers[0], foo_bar_baz());
    +    }
    +}
    
    From d7c9ad2ce64f886ae7e50781daf9d679d5252380 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Wed, 27 Oct 2021 13:27:22 +0900
    Subject: [PATCH 374/490] test: fix setUp()
    
    Ensure the database exists.
    ---
     tests/system/Commands/CreateDatabaseTest.php | 7 ++++++-
     1 file changed, 6 insertions(+), 1 deletion(-)
    
    diff --git a/tests/system/Commands/CreateDatabaseTest.php b/tests/system/Commands/CreateDatabaseTest.php
    index 1b6d28fdf0f8..a00de5c437a1 100644
    --- a/tests/system/Commands/CreateDatabaseTest.php
    +++ b/tests/system/Commands/CreateDatabaseTest.php
    @@ -12,6 +12,7 @@
     namespace CodeIgniter\Commands;
     
     use CodeIgniter\Database\BaseConnection;
    +use CodeIgniter\Database\Database as DatabaseFactory;
     use CodeIgniter\Database\SQLite3\Connection as SQLite3Connection;
     use CodeIgniter\Test\CIUnitTestCase;
     use CodeIgniter\Test\Filters\CITestStreamFilter;
    @@ -45,7 +46,11 @@ protected function setUp(): void
                     unlink($file);
                 }
             } else {
    -            Database::forge()->dropDatabase('foobar');
    +            $util = (new DatabaseFactory())->loadUtils($this->connection);
    +
    +            if ($util->databaseExists('foobar')) {
    +                Database::forge()->dropDatabase('foobar');
    +            }
             }
         }
     
    
    From f758702531514468d247796b4bef4d1eb864d697 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA"
     <51850998+paulbalandan@users.noreply.github.com>
    Date: Thu, 28 Oct 2021 10:51:13 +0800
    Subject: [PATCH 375/490] Apply suggestions from code review
    
    ---
     tests/system/CommonHelperTest.php | 2 --
     1 file changed, 2 deletions(-)
    
    diff --git a/tests/system/CommonHelperTest.php b/tests/system/CommonHelperTest.php
    index 55d626cf966c..72e6075f7d1f 100644
    --- a/tests/system/CommonHelperTest.php
    +++ b/tests/system/CommonHelperTest.php
    @@ -96,7 +96,6 @@ public function testHelperWithFatalLocatorThrowsException()
             }
     
             $this->assertTrue($exception);
    -        Services::reset();
         }
     
         public function testHelperLoadsOnce()
    @@ -116,7 +115,6 @@ public function testHelperLoadsOnce()
             }
     
             $this->assertFalse($exception);
    -        Services::reset();
         }
     
         public function testHelperLoadsAppHelperFirst(): void
    
    From 120a56a71826a7dbcef5ead218ac56ed292725e7 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA"
     <51850998+paulbalandan@users.noreply.github.com>
    Date: Thu, 28 Oct 2021 10:52:58 +0800
    Subject: [PATCH 376/490] Update tests/system/CommonHelperTest.php
    
    ---
     tests/system/CommonHelperTest.php | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/tests/system/CommonHelperTest.php b/tests/system/CommonHelperTest.php
    index 72e6075f7d1f..ed3d629266a4 100644
    --- a/tests/system/CommonHelperTest.php
    +++ b/tests/system/CommonHelperTest.php
    @@ -33,6 +33,7 @@ final class CommonHelperTest extends CIUnitTestCase
         protected function setUp(): void
         {
             parent::setUp();
    +        Services::reset();
             $this->cleanUpDummyHelpers();
         }
     
    
    From 568d8a98e4b94f7ab44a78cc6afcfd6144ddeea8 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Thu, 28 Oct 2021 11:53:57 +0900
    Subject: [PATCH 377/490] docs: decorate `.env` with `**`
    
    ---
     user_guide_src/source/database/configuration.rst       | 4 ++--
     user_guide_src/source/general/common_functions.rst     | 2 +-
     user_guide_src/source/general/configuration.rst        | 8 ++++----
     user_guide_src/source/general/errors.rst               | 2 +-
     user_guide_src/source/installation/running.rst         | 4 ++--
     user_guide_src/source/installation/troubleshooting.rst | 2 +-
     user_guide_src/source/libraries/encryption.rst         | 2 +-
     user_guide_src/source/libraries/honeypot.rst           | 2 +-
     8 files changed, 13 insertions(+), 13 deletions(-)
    
    diff --git a/user_guide_src/source/database/configuration.rst b/user_guide_src/source/database/configuration.rst
    index 53b2c3f932b0..f1776d93151d 100644
    --- a/user_guide_src/source/database/configuration.rst
    +++ b/user_guide_src/source/database/configuration.rst
    @@ -9,7 +9,7 @@ Database Configuration
     CodeIgniter has a config file that lets you store your database
     connection values (username, password, database name, etc.). The config
     file is located at **app/Config/Database.php**. You can also set
    -database connection values in the .env file. See below for more details.
    +database connection values in the **.env** file. See below for more details.
     
     The config settings are stored in a class property that is an array with this
     prototype::
    @@ -162,7 +162,7 @@ within the class' constructor::
     Configuring With .env File
     --------------------------
     
    -You can also save your configuration values within a ``.env`` file with the current server's
    +You can also save your configuration values within a **.env** file with the current server's
     database settings. You only need to enter the values that change from what is in the
     default group's configuration settings. The values should be name following this format, where
     ``default`` is the group name::
    diff --git a/user_guide_src/source/general/common_functions.rst b/user_guide_src/source/general/common_functions.rst
    index 8d006197bcfe..dd4c69a9915e 100755
    --- a/user_guide_src/source/general/common_functions.rst
    +++ b/user_guide_src/source/general/common_functions.rst
    @@ -63,7 +63,7 @@ Service Accessors
         or return a default value if it is not found. Will format boolean values
         to actual booleans instead of string representations.
     
    -    Especially useful when used in conjunction with .env files for setting
    +    Especially useful when used in conjunction with **.env** files for setting
         values that are specific to the environment itself, like database
         settings, API keys, etc.
     
    diff --git a/user_guide_src/source/general/configuration.rst b/user_guide_src/source/general/configuration.rst
    index 8254b9fe44ea..ac31e6f3241a 100644
    --- a/user_guide_src/source/general/configuration.rst
    +++ b/user_guide_src/source/general/configuration.rst
    @@ -191,15 +191,15 @@ Environment Variables as Replacements for Data
     ==============================================
     
     It is very important to always remember that environment variables contained in your **.env** are
    -**only replacements for existing data**. This means that you cannot expect to fill your ``.env`` with all
    +**only replacements for existing data**. This means that you cannot expect to fill your **.env** with all
     the replacements for your configurations but have nothing to receive these replacements in the
     related configuration file(s).
     
    -The ``.env`` only serves to fill or replace the values in your configuration files. That said, your
    +The **.env** only serves to fill or replace the values in your configuration files. That said, your
     configuration files should have a container or receiving property for those. Adding so many variables in
    -your ``.env`` with nothing to contain them in the receiving end is useless.
    +your **.env** with nothing to contain them in the receiving end is useless.
     
    -Simply put, you cannot just put ``app.myNewConfig = foo`` in your ``.env`` and expect your ``Config\App``
    +Simply put, you cannot just put ``app.myNewConfig = foo`` in your **.env** and expect your ``Config\App``
     to magically have that property and value at run time.
     
     Treating Environment Variables as Arrays
    diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst
    index 410e58236a8d..b7863b03010d 100644
    --- a/user_guide_src/source/general/errors.rst
    +++ b/user_guide_src/source/general/errors.rst
    @@ -55,7 +55,7 @@ Configuration
     
     By default, CodeIgniter will display all errors in the ``development`` and ``testing`` environments, and will not
     display any errors in the ``production`` environment. You can change this by setting the ``CI_ENVIRONMENT`` variable
    -in the ``.env`` file.
    +in the **.env** file.
     
     .. important:: Disabling error reporting DOES NOT stop logs from being written if there are errors.
     
    diff --git a/user_guide_src/source/installation/running.rst b/user_guide_src/source/installation/running.rst
    index 4d00260ffcea..bb4b99aa468f 100644
    --- a/user_guide_src/source/installation/running.rst
    +++ b/user_guide_src/source/installation/running.rst
    @@ -17,11 +17,11 @@ Initial Configuration & Set Up
     
     #. Open the **app/Config/App.php** file with a text editor and
        set your base URL. If you need more flexibility, the baseURL may
    -   be set within the ``.env`` file as **app.baseURL="http://example.com/"**.
    +   be set within the **.env** file as ``app.baseURL="http://example.com/"``.
        (Always use a trailing slash on your base URL!)
     #. If you intend to use a database, open the
        **app/Config/Database.php** file with a text editor and set your
    -   database settings. Alternately, these could be set in your ``.env`` file.
    +   database settings. Alternately, these could be set in your **.env** file.
     
     One additional measure to take in production environments is to disable
     PHP error reporting and any other development-only functionality. In
    diff --git a/user_guide_src/source/installation/troubleshooting.rst b/user_guide_src/source/installation/troubleshooting.rst
    index a0b1684fda9d..ff30759efcad 100644
    --- a/user_guide_src/source/installation/troubleshooting.rst
    +++ b/user_guide_src/source/installation/troubleshooting.rst
    @@ -65,7 +65,7 @@ unrecoverable error, which we don't want to show to the viewer of
     the webapp, for better security.
     
     You can see the error in the debug toolbar display by setting your environment to
    -"development" (in `.env`), and reloading the page.
    +"development" (in **.env**), and reloading the page.
     
     Don't forget to reset the environment to "production" once you fix the problem!
     
    diff --git a/user_guide_src/source/libraries/encryption.rst b/user_guide_src/source/libraries/encryption.rst
    index 2c8373743f35..2c45e7ad79d9 100644
    --- a/user_guide_src/source/libraries/encryption.rst
    +++ b/user_guide_src/source/libraries/encryption.rst
    @@ -153,7 +153,7 @@ the library.
         // or
         public $key = 'base64:'
     
    -Similarly, you can use these prefixes in your ``.env`` file, too!
    +Similarly, you can use these prefixes in your **.env** file, too!
     ::
     
         // For hex2bin
    diff --git a/user_guide_src/source/libraries/honeypot.rst b/user_guide_src/source/libraries/honeypot.rst
    index 471c0d520003..0b9114456cf0 100644
    --- a/user_guide_src/source/libraries/honeypot.rst
    +++ b/user_guide_src/source/libraries/honeypot.rst
    @@ -36,7 +36,7 @@ Customizing Honeypot
     =====================
     
     Honeypot can be customized. The fields below can be set either in
    -**app/Config/Honeypot.php** or in ``.env``.
    +**app/Config/Honeypot.php** or in **.env**.
     
     * ``hidden`` - true|false to control visibility of the honeypot field; default is ``true``
     * ``label`` - HTML label for the honeypot field, default is 'Fill This Field'
    
    From d263558af1dcc0148a58b3edb134df919e941b67 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA"
     <51850998+paulbalandan@users.noreply.github.com>
    Date: Thu, 28 Oct 2021 12:16:52 +0800
    Subject: [PATCH 378/490] Update tests/system/CommonHelperTest.php
    
    ---
     tests/system/CommonHelperTest.php | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/tests/system/CommonHelperTest.php b/tests/system/CommonHelperTest.php
    index ed3d629266a4..18e0a1022f8d 100644
    --- a/tests/system/CommonHelperTest.php
    +++ b/tests/system/CommonHelperTest.php
    @@ -32,8 +32,8 @@ final class CommonHelperTest extends CIUnitTestCase
     
         protected function setUp(): void
         {
    -        parent::setUp();
             Services::reset();
    +        parent::setUp();
             $this->cleanUpDummyHelpers();
         }
     
    
    From e307ef566c6f5d1ddeb611275cbba66faa490b89 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Thu, 28 Oct 2021 13:29:33 +0900
    Subject: [PATCH 379/490] fix: remove populateHeaders() not to share request
     headers form browser between CURL requests
    
    ---
     system/HTTP/CURLRequest.php           | 37 ++-------------------------
     tests/system/HTTP/CURLRequestTest.php | 23 -----------------
     2 files changed, 2 insertions(+), 58 deletions(-)
    
    diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php
    index 2a305fa1c802..0b6b1cfb9005 100644
    --- a/system/HTTP/CURLRequest.php
    +++ b/system/HTTP/CURLRequest.php
    @@ -368,27 +368,17 @@ public function send(string $method, string $url)
         }
     
         /**
    -     * Takes all headers current part of this request and adds them
    -     * to the cURL request.
    +     * Adds $this->headers to the cURL request.
          */
         protected function applyRequestHeaders(array $curlOptions = []): array
         {
             if (empty($this->headers)) {
    -            $this->populateHeaders();
    -            // Otherwise, it will corrupt the request
    -            $this->removeHeader('Host');
    -            $this->removeHeader('Accept-Encoding');
    -        }
    -
    -        $headers = $this->headers();
    -
    -        if (empty($headers)) {
                 return $curlOptions;
             }
     
             $set = [];
     
    -        foreach (array_keys($headers) as $name) {
    +        foreach (array_keys($this->headers) as $name) {
                 $set[] = $name . ': ' . $this->getHeaderLine($name);
             }
     
    @@ -397,29 +387,6 @@ protected function applyRequestHeaders(array $curlOptions = []): array
             return $curlOptions;
         }
     
    -    /**
    -     * Override
    -     */
    -    public function populateHeaders(): void
    -    {
    -        foreach (array_keys($_SERVER) as $key) {
    -            if (sscanf($key, 'HTTP_%s', $header) === 1) {
    -                // take SOME_HEADER and turn it into Some-Header
    -                $header = str_replace('_', ' ', strtolower($header));
    -                $header = str_replace(' ', '-', ucwords($header));
    -
    -                if (in_array($header, $this->unsharedHeaders, true)) {
    -                    continue;
    -                }
    -
    -                $this->setHeader($header, $_SERVER[$key]);
    -
    -                // Add us to the header map so we can find them case-insensitively
    -                $this->headerMap[strtolower($header)] = $header;
    -            }
    -        }
    -    }
    -
         /**
          * Apply method
          */
    diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php
    index b46cec5ce992..33fb4d2adfdb 100644
    --- a/tests/system/HTTP/CURLRequestTest.php
    +++ b/tests/system/HTTP/CURLRequestTest.php
    @@ -173,29 +173,6 @@ public function testOptionsHeaders()
             $this->assertSame('apple', $request->header('fruit')->getValue());
         }
     
    -    /**
    -     * @backupGlobals enabled
    -     */
    -    public function testOptionsHeadersUsingPopulate()
    -    {
    -        $_SERVER['HTTP_HOST']            = 'site1.com';
    -        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en-US';
    -        $_SERVER['HTTP_ACCEPT_ENCODING'] = 'gzip, deflate, br';
    -
    -        $options = [
    -            'base_uri' => 'http://www.foo.com/api/v1/',
    -        ];
    -
    -        $request = $this->getRequest($options);
    -        $request->get('example');
    -        // we fill the Accept-Language header from _SERVER when no headers are defined for the request
    -        $this->assertSame('en-US', $request->header('Accept-Language')->getValue());
    -        // but we skip Host header - since it would corrupt the request
    -        $this->assertNull($request->header('Host'));
    -        // and Accept-Encoding
    -        $this->assertNull($request->header('Accept-Encoding'));
    -    }
    -
         /**
          * @backupGlobals enabled
          */
    
    From 0b18ab84474ef9733487fd3b48eea135b0cd4569 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Thu, 28 Oct 2021 17:10:54 +0900
    Subject: [PATCH 380/490] style: change global_namespace_import to true in
     php-cs-fixer
    
    https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/2.19/doc/rules/import/global_namespace_import.rst
    ---
     tests/_support/Commands/AppInfo.php        |  3 ++-
     tests/_support/Commands/InvalidCommand.php |  3 ++-
     tests/_support/Controllers/Popcorn.php     |  3 ++-
     tests/_support/Widgets/SomeWidget.php      |  4 +++-
     tests/system/I18n/TimeTest.php             | 20 ++++++++++----------
     5 files changed, 19 insertions(+), 14 deletions(-)
    
    diff --git a/tests/_support/Commands/AppInfo.php b/tests/_support/Commands/AppInfo.php
    index 562cc9dd3847..e7bf3093e658 100644
    --- a/tests/_support/Commands/AppInfo.php
    +++ b/tests/_support/Commands/AppInfo.php
    @@ -14,6 +14,7 @@
     use CodeIgniter\CLI\BaseCommand;
     use CodeIgniter\CLI\CLI;
     use CodeIgniter\CodeIgniter;
    +use RuntimeException;
     
     class AppInfo extends BaseCommand
     {
    @@ -31,7 +32,7 @@ public function bomb()
         {
             try {
                 CLI::color('test', 'white', 'Background');
    -        } catch (\RuntimeException $oops) {
    +        } catch (RuntimeException $oops) {
                 $this->showError($oops);
             }
         }
    diff --git a/tests/_support/Commands/InvalidCommand.php b/tests/_support/Commands/InvalidCommand.php
    index a6d13d6fe494..5b4acf936b08 100644
    --- a/tests/_support/Commands/InvalidCommand.php
    +++ b/tests/_support/Commands/InvalidCommand.php
    @@ -14,6 +14,7 @@
     use CodeIgniter\CLI\BaseCommand;
     use CodeIgniter\CLI\CLI;
     use CodeIgniter\CodeIgniter;
    +use ReflectionException;
     
     class InvalidCommand extends BaseCommand
     {
    @@ -23,7 +24,7 @@ class InvalidCommand extends BaseCommand
     
         public function __construct()
         {
    -        throw new \ReflectionException();
    +        throw new ReflectionException();
         }
     
         public function run(array $params)
    diff --git a/tests/_support/Controllers/Popcorn.php b/tests/_support/Controllers/Popcorn.php
    index a0aa972c635f..71a316685a32 100644
    --- a/tests/_support/Controllers/Popcorn.php
    +++ b/tests/_support/Controllers/Popcorn.php
    @@ -13,6 +13,7 @@
     
     use CodeIgniter\API\ResponseTrait;
     use CodeIgniter\Controller;
    +use RuntimeException;
     
     /**
      * This is a testing only controller, intended to blow up in multiple
    @@ -34,7 +35,7 @@ public function pop()
     
         public function popper()
         {
    -        throw new \RuntimeException('Surprise', 500);
    +        throw new RuntimeException('Surprise', 500);
         }
     
         public function weasel()
    diff --git a/tests/_support/Widgets/SomeWidget.php b/tests/_support/Widgets/SomeWidget.php
    index 380449f14b9b..c6e2b3da8652 100644
    --- a/tests/_support/Widgets/SomeWidget.php
    +++ b/tests/_support/Widgets/SomeWidget.php
    @@ -11,7 +11,9 @@
     
     namespace Tests\Support\Widgets;
     
    +use stdClass;
    +
     // Extends a trivial class to test the instanceOf directive
    -class SomeWidget extends \stdClass
    +class SomeWidget extends stdClass
     {
     }
    diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php
    index da1ef5b3c5ef..e1e683c98d07 100644
    --- a/tests/system/I18n/TimeTest.php
    +++ b/tests/system/I18n/TimeTest.php
    @@ -90,7 +90,7 @@ public function testTimeWithDateTimeZone()
                 'yyyy-MM-dd HH:mm:ss'
             );
     
    -        $time = new Time('now', new \DateTimeZone('Europe/London'), 'fr_FR');
    +        $time = new Time('now', new DateTimeZone('Europe/London'), 'fr_FR');
     
             $this->assertSame($formatter->format($time), (string) $time);
         }
    @@ -101,13 +101,13 @@ public function testToDateTime()
     
             $obj = $time->toDateTime();
     
    -        $this->assertInstanceOf(\DateTime::class, $obj);
    +        $this->assertInstanceOf(DateTime::class, $obj);
         }
     
         public function testNow()
         {
             $time  = Time::now();
    -        $time1 = new \DateTime();
    +        $time1 = new DateTime();
     
             $this->assertInstanceOf(Time::class, $time);
             $this->assertSame($time->getTimestamp(), $time1->getTimestamp());
    @@ -116,7 +116,7 @@ public function testNow()
         public function testParse()
         {
             $time  = Time::parse('next Tuesday', 'America/Chicago');
    -        $time1 = new \DateTime('now', new \DateTimeZone('America/Chicago'));
    +        $time1 = new DateTime('now', new DateTimeZone('America/Chicago'));
             $time1->modify('next Tuesday');
     
             $this->assertSame($time->getTimestamp(), $time1->getTimestamp());
    @@ -134,7 +134,7 @@ public function testToDateTimeStringWithTimeZone()
         {
             $time = Time::parse('2017-01-12 00:00', 'Europe/London');
     
    -        $expects = new \DateTime('2017-01-12', new \DateTimeZone('Europe/London'));
    +        $expects = new DateTime('2017-01-12', new DateTimeZone('Europe/London'));
     
             $this->assertSame($expects->format('Y-m-d H:i:s'), $time->toDateTimeString());
         }
    @@ -204,7 +204,7 @@ public function testCreateFromTimeLocalized()
     
         public function testCreateFromFormat()
         {
    -        $now = new \DateTime('now');
    +        $now = new DateTime('now');
     
             Time::setTestNow($now);
             $time = Time::createFromFormat('F j, Y', 'January 15, 2017', 'America/Chicago');
    @@ -222,7 +222,7 @@ public function testCreateFromFormatWithTimezoneString()
     
         public function testCreateFromFormatWithTimezoneObject()
         {
    -        $tz = new \DateTimeZone('Europe/London');
    +        $tz = new DateTimeZone('Europe/London');
     
             $time = Time::createFromFormat('F j, Y', 'January 15, 2017', $tz);
     
    @@ -422,7 +422,7 @@ public function testGetTimezone()
         {
             $instance = Time::now()->getTimezone();
     
    -        $this->assertInstanceOf(\DateTimeZone::class, $instance);
    +        $this->assertInstanceOf(DateTimeZone::class, $instance);
         }
     
         public function testGetTimezonename()
    @@ -776,7 +776,7 @@ public function testEqualWithSame()
         public function testEqualWithDateTime()
         {
             $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago');
    -        $time2 = new \DateTime('January 11, 2017 03:50:00', new \DateTimeZone('Europe/London'));
    +        $time2 = new DateTime('January 11, 2017 03:50:00', new DateTimeZone('Europe/London'));
     
             $this->assertTrue($time1->equals($time2));
         }
    @@ -784,7 +784,7 @@ public function testEqualWithDateTime()
         public function testEqualWithSameDateTime()
         {
             $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago');
    -        $time2 = new \DateTime('January 10, 2017 21:50:00', new \DateTimeZone('America/Chicago'));
    +        $time2 = new DateTime('January 10, 2017 21:50:00', new DateTimeZone('America/Chicago'));
     
             $this->assertTrue($time1->equals($time2));
         }
    
    From 9cbe0f25159d7cbf2fe18d99b60c905a2a9f3701 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 10:26:32 +0900
    Subject: [PATCH 381/490] fix: make $unsharedHeaders protected
    
    To override the values.
    ---
     system/HTTP/CURLRequest.php | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php
    index 0b6b1cfb9005..baf97d2b891d 100644
    --- a/system/HTTP/CURLRequest.php
    +++ b/system/HTTP/CURLRequest.php
    @@ -77,7 +77,7 @@ class CURLRequest extends Request
          *
          * @var string[]
          */
    -    private $unsharedHeaders = [
    +    protected $unsharedHeaders = [
             'Content-Length',
             'Content-Type',
         ];
    
    From c9bac5f99a40ebc878c245efb1745b84b84d8a9d Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 11:16:39 +0900
    Subject: [PATCH 382/490] fix: reset $unsharedHeaders after sending request
    
    If user setHeader() that is defined in $unsharedHeaders, the header will be removed
    before sending request, and there is no way to send the header.
    
    The reason I didn't do the reset after sending is because a test failed.
    This time I modify the test, too.
    ---
     system/HTTP/CURLRequest.php           | 9 ++++-----
     tests/system/HTTP/CURLRequestTest.php | 5 ++++-
     2 files changed, 8 insertions(+), 6 deletions(-)
    
    diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php
    index baf97d2b891d..b195249589e9 100644
    --- a/system/HTTP/CURLRequest.php
    +++ b/system/HTTP/CURLRequest.php
    @@ -116,11 +116,6 @@ public function __construct(App $config, URI $uri, ?ResponseInterface $response
          */
         public function request($method, string $url, array $options = []): ResponseInterface
         {
    -        // Reset unshared headers
    -        foreach ($this->unsharedHeaders as $header) {
    -            $this->removeHeader($header);
    -        }
    -
             $this->parseOptions($options);
     
             $url = $this->prepareURL($url);
    @@ -129,6 +124,10 @@ public function request($method, string $url, array $options = []): ResponseInte
     
             $this->send($method, $url);
     
    +        // Reset unshared headers
    +        foreach ($this->unsharedHeaders as $header) {
    +            $this->removeHeader($header);
    +        }
             // Reset unshared configs
             unset($this->config['multipart'], $this->config['form_params']);
     
    diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php
    index 33fb4d2adfdb..435a647ecf90 100644
    --- a/tests/system/HTTP/CURLRequestTest.php
    +++ b/tests/system/HTTP/CURLRequestTest.php
    @@ -912,7 +912,10 @@ public function testSetJSON()
             $this->request->setJSON($params)->post('/post');
     
             $this->assertSame(json_encode($params), $this->request->getBody());
    -        $this->assertSame('application/json', $this->request->getHeaderLine('Content-Type'));
    +        $this->assertSame(
    +            'Content-Type: application/json',
    +            $this->request->curl_options[CURLOPT_HTTPHEADER][0]
    +        );
         }
     
         public function testHTTPv1()
    
    From d39543ece518bade48715451e69992aacefa1ca2 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 11:33:31 +0900
    Subject: [PATCH 383/490] docs: improve changelogs/v4.1.5.rst format
    
    ---
     user_guide_src/source/changelogs/v4.1.5.rst | 25 +++++++++++++--------
     1 file changed, 16 insertions(+), 9 deletions(-)
    
    diff --git a/user_guide_src/source/changelogs/v4.1.5.rst b/user_guide_src/source/changelogs/v4.1.5.rst
    index 3290763f0ed3..d1d9fd58aa2c 100644
    --- a/user_guide_src/source/changelogs/v4.1.5.rst
    +++ b/user_guide_src/source/changelogs/v4.1.5.rst
    @@ -1,28 +1,35 @@
     Version 4.1.5
    -=============
    +#############
     
     Release Date: Not released
     
     **4.1.5 release of CodeIgniter4**
     
    -BREAKING:
    +.. contents::
    +    :local:
    +    :depth: 1
     
    -Fixed `a bug `_ on CSRF protection.
    -Now CSRF protection works on PUT/PATCH/DELETE requests when CSRF filter is applied.
    -If you use such requests, you need to send CSRF token.
    +BREAKING
    +========
     
    -Enhancements:
    +- Fixed `a bug `_ on CSRF protection. Now CSRF protection works on PUT/PATCH/DELETE requests when CSRF filter is applied. If you use such requests, you need to send CSRF token.
    +
    +Enhancements
    +============
     
     - Added Cache config for reserved characters
     - The ``addForeignKey`` function of the ``Forge`` class can now define composite foreign keys in an array
     - The ``dropKey`` function of the ``Forge`` class can remove key
     
    -Changes:
    +Changes
    +=======
     
     - Always escape identifiers in the ``set``, ``setUpdateBatch``, and ``insertBatch`` functions in ``BaseBuilder``.
     
    -Deprecations:
    +Deprecations
    +============
     
     - Deprecated ``CodeIgniter\\Cache\\Handlers\\BaseHandler::RESERVED_CHARACTERS`` in favor of the new config property
     
    -Bugs Fixed:
    +Bugs Fixed
    +==========
    
    From 9a6a0d3e708e1dbca2b8efb48f16ac00cbe492cf Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 11:38:33 +0900
    Subject: [PATCH 384/490] docs: add upgrading and changelog
    
    ---
     user_guide_src/source/changelogs/v4.1.5.rst        | 1 +
     user_guide_src/source/installation/upgrade_415.rst | 8 ++++++++
     2 files changed, 9 insertions(+)
    
    diff --git a/user_guide_src/source/changelogs/v4.1.5.rst b/user_guide_src/source/changelogs/v4.1.5.rst
    index d1d9fd58aa2c..6e9c96ded738 100644
    --- a/user_guide_src/source/changelogs/v4.1.5.rst
    +++ b/user_guide_src/source/changelogs/v4.1.5.rst
    @@ -13,6 +13,7 @@ BREAKING
     ========
     
     - Fixed `a bug `_ on CSRF protection. Now CSRF protection works on PUT/PATCH/DELETE requests when CSRF filter is applied. If you use such requests, you need to send CSRF token.
    +- In the previous version, ``CURLRequest`` sends request headers from the browser, because of a bug. Since this version it does not send them.
     
     Enhancements
     ============
    diff --git a/user_guide_src/source/installation/upgrade_415.rst b/user_guide_src/source/installation/upgrade_415.rst
    index 6c20797775af..84750e834c12 100644
    --- a/user_guide_src/source/installation/upgrade_415.rst
    +++ b/user_guide_src/source/installation/upgrade_415.rst
    @@ -53,6 +53,14 @@ If you want the same behavior as the previous version, set the CSRF filter like
     
     Protecting **GET** method needs only when you use ``form_open()`` auto-generation of CSRF field.
     
    +CURLRequest header change
    +-------------------------
    +
    +Because of a bug, the previous version of ``CURLRequest`` sends the request headers from the browser request.
    +The bug was fixed. If your requests depend on the headers, your requests might fail after upgrading.
    +In this case, add the necessary headers manually.
    +See `CURLRequest Class <../libraries/curlrequest.html#headers>`_ for how to add.
    +
     Breaking Enhancements
     =====================
     
    
    From f5c1d2dbece72deb733e55368ccac173188eb205 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 13:17:27 +0900
    Subject: [PATCH 385/490] docs: fix method name
    
    ---
     user_guide_src/source/helpers/security_helper.rst | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/user_guide_src/source/helpers/security_helper.rst b/user_guide_src/source/helpers/security_helper.rst
    index b0d93bf81eec..2c3812a49def 100644
    --- a/user_guide_src/source/helpers/security_helper.rst
    +++ b/user_guide_src/source/helpers/security_helper.rst
    @@ -27,7 +27,7 @@ The following functions are available:
     
         Provides protection against directory traversal.
     
    -    This function is an alias for ``\CodeIgniter\Security::sanitize_filename()``.
    +    This function is an alias for ``\CodeIgniter\Security::sanitizeFilename()``.
         For more info, please see the :doc:`Security Library <../libraries/security>`
         documentation.
     
    
    From 0a38cf342f3736c6710c25eb0910e98db95e6658 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 13:20:36 +0900
    Subject: [PATCH 386/490] docs: replace csrf with CSRF
    
    ---
     user_guide_src/source/installation/upgrade_security.rst | 6 +++---
     1 file changed, 3 insertions(+), 3 deletions(-)
    
    diff --git a/user_guide_src/source/installation/upgrade_security.rst b/user_guide_src/source/installation/upgrade_security.rst
    index 5f2d8ecfdb66..606ac4139fa8 100644
    --- a/user_guide_src/source/installation/upgrade_security.rst
    +++ b/user_guide_src/source/installation/upgrade_security.rst
    @@ -13,15 +13,15 @@ Documentations
     - :doc:`Security Documentation Codeigniter 4.X `
     
     .. note::
    -    If you use the `form helper `_, then form_open() will automatically insert a hidden csrf field in your forms. So you do not have to upgrade this by yourself.
    +    If you use the `form helper `_, then form_open() will automatically insert a hidden CSRF field in your forms. So you do not have to upgrade this by yourself.
     
     What has been changed
     =====================
    -- The method to implement csrf tokens to html forms has been changed.
    +- The method to implement CSRF tokens to html forms has been changed.
     
     Upgrade Guide
     =============
    -1. To enable csrf protection in CI4 you have to enable it in ``app/Config/Filters.php``::
    +1. To enable CSRF protection in CI4 you have to enable it in ``app/Config/Filters.php``::
     
         public $globals = [
             'before' => [
    
    From 06cc51c18ddb0a8d5d3f0acc802edb568ab0d3d0 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 13:23:21 +0900
    Subject: [PATCH 387/490] docs: fix 404 link and make the note more accurate
    
    ---
     user_guide_src/source/installation/upgrade_security.rst | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/user_guide_src/source/installation/upgrade_security.rst b/user_guide_src/source/installation/upgrade_security.rst
    index 606ac4139fa8..7f991b0bdb47 100644
    --- a/user_guide_src/source/installation/upgrade_security.rst
    +++ b/user_guide_src/source/installation/upgrade_security.rst
    @@ -13,7 +13,7 @@ Documentations
     - :doc:`Security Documentation Codeigniter 4.X `
     
     .. note::
    -    If you use the `form helper `_, then form_open() will automatically insert a hidden CSRF field in your forms. So you do not have to upgrade this by yourself.
    +    If you use the :doc:`form helper ` and enable the CSRF filter globally, then ``form_open()`` will automatically insert a hidden CSRF field in your forms. So you do not have to upgrade this by yourself.
     
     What has been changed
     =====================
    
    From 613b9809b5b58ff35a39aa553ccf9a76bce82413 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 16:00:59 +0900
    Subject: [PATCH 388/490] fix: remove $unsharedHeaders and add $defaultOptions
    
    $defaultOptions is options passed in the constructor.
    They are applied for all requests.
    All request headers are cleared after sending request,
    and $defaultOptions are set again.
    ---
     system/HTTP/CURLRequest.php | 23 +++++++++++------------
     1 file changed, 11 insertions(+), 12 deletions(-)
    
    diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php
    index b195249589e9..4d2afdbfd856 100644
    --- a/system/HTTP/CURLRequest.php
    +++ b/system/HTTP/CURLRequest.php
    @@ -73,14 +73,11 @@ class CURLRequest extends Request
         protected $delay = 0.0;
     
         /**
    -     * Request headers that are not shared between requests.
    +     * Default options. Applied to all requests.
          *
    -     * @var string[]
    +     * @var array
          */
    -    protected $unsharedHeaders = [
    -        'Content-Length',
    -        'Content-Type',
    -    ];
    +    private $defaultOptions;
     
         /**
          * Takes an array of options to set the following possible class properties:
    @@ -102,8 +99,9 @@ public function __construct(App $config, URI $uri, ?ResponseInterface $response
     
             parent::__construct($config);
     
    -        $this->response = $response;
    -        $this->baseURI  = $uri->useRawQueryString();
    +        $this->response       = $response;
    +        $this->baseURI        = $uri->useRawQueryString();
    +        $this->defaultOptions = $options;
     
             $this->parseOptions($options);
         }
    @@ -124,12 +122,13 @@ public function request($method, string $url, array $options = []): ResponseInte
     
             $this->send($method, $url);
     
    -        // Reset unshared headers
    -        foreach ($this->unsharedHeaders as $header) {
    -            $this->removeHeader($header);
    -        }
    +        // Reset headers
    +        $this->headers   = [];
    +        $this->headerMap = [];
             // Reset unshared configs
             unset($this->config['multipart'], $this->config['form_params']);
    +        // Set the default options for next request
    +        $this->parseOptions($this->defaultOptions);
     
             return $this->response;
         }
    
    From 8b086428d951f031c979423a61a18704082bc972 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 16:22:44 +0900
    Subject: [PATCH 389/490] docs: fix changelogs/v4.1.5.rst
    
    The explanation was not accurate.
    
    Co-authored-by: Michal Sniatala 
    ---
     user_guide_src/source/changelogs/v4.1.5.rst | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/user_guide_src/source/changelogs/v4.1.5.rst b/user_guide_src/source/changelogs/v4.1.5.rst
    index 6e9c96ded738..f763123fbd85 100644
    --- a/user_guide_src/source/changelogs/v4.1.5.rst
    +++ b/user_guide_src/source/changelogs/v4.1.5.rst
    @@ -13,7 +13,7 @@ BREAKING
     ========
     
     - Fixed `a bug `_ on CSRF protection. Now CSRF protection works on PUT/PATCH/DELETE requests when CSRF filter is applied. If you use such requests, you need to send CSRF token.
    -- In the previous version, ``CURLRequest`` sends request headers from the browser, because of a bug. Since this version it does not send them.
    +- In the previous version, if you didn't provide your own headers, ``CURLRequest`` would send the request-headers from the browser, due to a bug. As of this version, it does not send them.
     
     Enhancements
     ============
    
    From 182c25264332e5a5331a1025bf33afc4cd2e6fdb Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 16:23:40 +0900
    Subject: [PATCH 390/490] docs: fix upgrade_415.rst
    
    The explanation was not accurate.
    
    Co-authored-by: Michal Sniatala 
    ---
     user_guide_src/source/installation/upgrade_415.rst | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/user_guide_src/source/installation/upgrade_415.rst b/user_guide_src/source/installation/upgrade_415.rst
    index 84750e834c12..7a8c51f985ae 100644
    --- a/user_guide_src/source/installation/upgrade_415.rst
    +++ b/user_guide_src/source/installation/upgrade_415.rst
    @@ -56,7 +56,7 @@ Protecting **GET** method needs only when you use ``form_open()`` auto-generatio
     CURLRequest header change
     -------------------------
     
    -Because of a bug, the previous version of ``CURLRequest`` sends the request headers from the browser request.
    +In the previous version, if you didn't provide your own headers, ``CURLRequest`` would send the request-headers from the browser.
     The bug was fixed. If your requests depend on the headers, your requests might fail after upgrading.
     In this case, add the necessary headers manually.
     See `CURLRequest Class <../libraries/curlrequest.html#headers>`_ for how to add.
    
    From aed89d9119f6ec811757bb7c4fadf1b42e2242d9 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 17:44:10 +0900
    Subject: [PATCH 391/490] fix: add config $CURLRequestShareOptions for the
     CURLRequest
    
    If $CURLRequestShareOptions is false, reset all config after sending a request.
    If true, keep the all config (the same as before).
    ---
     app/Config/App.php                    | 11 +++++++++
     system/HTTP/CURLRequest.php           | 35 +++++++++++++++++++++++----
     tests/system/HTTP/CURLRequestTest.php | 12 +++++----
     3 files changed, 48 insertions(+), 10 deletions(-)
    
    diff --git a/app/Config/App.php b/app/Config/App.php
    index 88b295e9a010..57c9b8a45c46 100644
    --- a/app/Config/App.php
    +++ b/app/Config/App.php
    @@ -461,4 +461,15 @@ class App extends BaseConfig
          * @var bool
          */
         public $CSPEnabled = false;
    +
    +    /**
    +     * --------------------------------------------------------------------------
    +     * CURLRequest Share Options
    +     * --------------------------------------------------------------------------
    +     *
    +     * Whether share options between requests or not.
    +     *
    +     * @var bool
    +     */
    +    public $CURLRequestShareOptions = true;
     }
    diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php
    index 4d2afdbfd856..4a58e5992afc 100644
    --- a/system/HTTP/CURLRequest.php
    +++ b/system/HTTP/CURLRequest.php
    @@ -42,7 +42,14 @@ class CURLRequest extends Request
          *
          * @var array
          */
    -    protected $config = [
    +    protected $config;
    +
    +    /**
    +     * The default setting values
    +     *
    +     * @var array
    +     */
    +    private $defaultConfig = [
             'timeout'         => 0.0,
             'connect_timeout' => 150,
             'debug'           => false,
    @@ -79,6 +86,13 @@ class CURLRequest extends Request
          */
         private $defaultOptions;
     
    +    /**
    +     * Whether share options between requests or not.
    +     *
    +     * @var bool
    +     */
    +    private $shareOptions;
    +
         /**
          * Takes an array of options to set the following possible class properties:
          *
    @@ -102,6 +116,8 @@ public function __construct(App $config, URI $uri, ?ResponseInterface $response
             $this->response       = $response;
             $this->baseURI        = $uri->useRawQueryString();
             $this->defaultOptions = $options;
    +        $this->config         = $this->defaultConfig;
    +        $this->shareOptions   = $config->CURLRequestShareOptions ?? true;
     
             $this->parseOptions($options);
         }
    @@ -122,15 +138,24 @@ public function request($method, string $url, array $options = []): ResponseInte
     
             $this->send($method, $url);
     
    +        if ($this->shareOptions === false) {
    +            $this->resetOptions();
    +        }
    +
    +        return $this->response;
    +    }
    +
    +    private function resetOptions()
    +    {
             // Reset headers
             $this->headers   = [];
             $this->headerMap = [];
    -        // Reset unshared configs
    -        unset($this->config['multipart'], $this->config['form_params']);
    +
    +        // Reset configs
    +        $this->config = $this->defaultConfig;
    +
             // Set the default options for next request
             $this->parseOptions($this->defaultOptions);
    -
    -        return $this->response;
         }
     
         /**
    diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php
    index 435a647ecf90..7237323f0be4 100644
    --- a/tests/system/HTTP/CURLRequestTest.php
    +++ b/tests/system/HTTP/CURLRequestTest.php
    @@ -36,11 +36,13 @@ protected function setUp(): void
             $this->request = $this->getRequest();
         }
     
    -    protected function getRequest(array $options = [])
    +    protected function getRequest(array $options = [], $shareOptions = true)
         {
    -        $uri = isset($options['base_uri']) ? new URI($options['base_uri']) : new URI();
    +        $uri                          = isset($options['base_uri']) ? new URI($options['base_uri']) : new URI();
    +        $app                          = new App();
    +        $app->CURLRequestShareOptions = $shareOptions;
     
    -        return new MockCURLRequest(($app = new App()), $uri, new Response($app), $options);
    +        return new MockCURLRequest(($app), $uri, new Response($app), $options);
         }
     
         /**
    @@ -197,12 +199,12 @@ public function testOptionsHeadersNotUsingPopulate()
             $this->assertSame('', $request->header('Accept-Encoding')->getValue());
         }
     
    -    public function testHeaderContentLengthNotSharedBetweenRequests()
    +    public function testHeaderContentLengthNotSharedBetweenRequestsWhenSharedOptionsFalse()
         {
             $options = [
                 'base_uri' => 'http://www.foo.com/api/v1/',
             ];
    -        $request = $this->getRequest($options);
    +        $request = $this->getRequest($options, false);
     
             $request->post('example', [
                 'form_params' => [
    
    From 436013e118aa681bdaf4bdfd5c93125fe5fbe224 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 18:42:50 +0900
    Subject: [PATCH 392/490] test: add tests for $CURLRequestShareOptions = false
    
    ---
     .../HTTP/CURLRequestDoNotShareOptionsTest.php | 975 ++++++++++++++++++
     tests/system/HTTP/CURLRequestTest.php         |  21 +-
     2 files changed, 977 insertions(+), 19 deletions(-)
     create mode 100644 tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    
    diff --git a/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    new file mode 100644
    index 000000000000..d40356118f79
    --- /dev/null
    +++ b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    @@ -0,0 +1,975 @@
    +
    + *
    + * For the full copyright and license information, please view
    + * the LICENSE file that was distributed with this source code.
    + */
    +
    +namespace CodeIgniter\HTTP;
    +
    +use CodeIgniter\Config\Services;
    +use CodeIgniter\HTTP\Exceptions\HTTPException;
    +use CodeIgniter\Test\CIUnitTestCase;
    +use CodeIgniter\Test\Mock\MockCURLRequest;
    +use Config\App;
    +use CURLFile;
    +
    +/**
    + * @internal
    + */
    +final class CURLRequestDoNotShareOptionsTest extends CIUnitTestCase
    +{
    +    /**
    +     * @var MockCURLRequest
    +     */
    +    protected $request;
    +
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +
    +        Services::reset();
    +        $this->request = $this->getRequest();
    +    }
    +
    +    protected function getRequest(array $options = [])
    +    {
    +        $uri                          = isset($options['base_uri']) ? new URI($options['base_uri']) : new URI();
    +        $app                          = new App();
    +        $app->CURLRequestShareOptions = false;
    +
    +        return new MockCURLRequest(($app), $uri, new Response($app), $options);
    +    }
    +
    +    /**
    +     * @see https://github.com/codeigniter4/CodeIgniter4/issues/4707
    +     */
    +    public function testPrepareURLIgnoresAppConfig()
    +    {
    +        config('App')->baseURL = 'http://example.com/fruit/';
    +
    +        $request = $this->getRequest(['base_uri' => 'http://example.com/v1/']);
    +
    +        $method = $this->getPrivateMethodInvoker($request, 'prepareURL');
    +
    +        $this->assertSame('http://example.com/v1/bananas', $method('bananas'));
    +    }
    +
    +    /**
    +     * @see https://github.com/codeigniter4/CodeIgniter4/issues/1029
    +     */
    +    public function testGetRemembersBaseURI()
    +    {
    +        $request = $this->getRequest(['base_uri' => 'http://www.foo.com/api/v1/']);
    +
    +        $request->get('products');
    +
    +        $options = $request->curl_options;
    +
    +        $this->assertSame('http://www.foo.com/api/v1/products', $options[CURLOPT_URL]);
    +    }
    +
    +    /**
    +     * @see https://github.com/codeigniter4/CodeIgniter4/issues/1029
    +     */
    +    public function testGetRemembersBaseURIWithHelperMethod()
    +    {
    +        $request = Services::curlrequest(['base_uri' => 'http://www.foo.com/api/v1/']);
    +
    +        $uri = $this->getPrivateProperty($request, 'baseURI');
    +        $this->assertSame('www.foo.com', $uri->getHost());
    +        $this->assertSame('/api/v1/', $uri->getPath());
    +    }
    +
    +    public function testSendReturnsResponse()
    +    {
    +        $output = 'Howdy Stranger.';
    +
    +        $response = $this->request->setOutput($output)->send('get', 'http://example.com');
    +
    +        $this->assertInstanceOf('CodeIgniter\\HTTP\\Response', $response);
    +        $this->assertSame($output, $response->getBody());
    +    }
    +
    +    public function testGetSetsCorrectMethod()
    +    {
    +        $this->request->get('http://example.com');
    +
    +        $this->assertSame('get', $this->request->getMethod());
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options);
    +        $this->assertSame('GET', $options[CURLOPT_CUSTOMREQUEST]);
    +    }
    +
    +    public function testDeleteSetsCorrectMethod()
    +    {
    +        $this->request->delete('http://example.com');
    +
    +        $this->assertSame('delete', $this->request->getMethod());
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options);
    +        $this->assertSame('DELETE', $options[CURLOPT_CUSTOMREQUEST]);
    +    }
    +
    +    public function testHeadSetsCorrectMethod()
    +    {
    +        $this->request->head('http://example.com');
    +
    +        $this->assertSame('head', $this->request->getMethod());
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options);
    +        $this->assertSame('HEAD', $options[CURLOPT_CUSTOMREQUEST]);
    +    }
    +
    +    public function testOptionsSetsCorrectMethod()
    +    {
    +        $this->request->options('http://example.com');
    +
    +        $this->assertSame('options', $this->request->getMethod());
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options);
    +        $this->assertSame('OPTIONS', $options[CURLOPT_CUSTOMREQUEST]);
    +    }
    +
    +    public function testOptionsBaseURIOption()
    +    {
    +        $options = ['base_uri' => 'http://www.foo.com/api/v1/'];
    +        $request = $this->getRequest($options);
    +
    +        $this->assertSame('http://www.foo.com/api/v1/', $request->getBaseURI()->__toString());
    +    }
    +
    +    public function testOptionsBaseURIOverride()
    +    {
    +        $options = [
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +            'baseURI'  => 'http://bogus/com',
    +        ];
    +        $request = $this->getRequest($options);
    +
    +        $this->assertSame('http://bogus/com', $request->getBaseURI()->__toString());
    +    }
    +
    +    public function testOptionsHeaders()
    +    {
    +        $options = [
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +            'headers'  => ['fruit' => 'apple'],
    +        ];
    +        $request = $this->getRequest();
    +        $this->assertNull($request->header('fruit'));
    +
    +        $request = $this->getRequest($options);
    +        $this->assertSame('apple', $request->header('fruit')->getValue());
    +    }
    +
    +    /**
    +     * @backupGlobals enabled
    +     */
    +    public function testOptionsHeadersNotUsingPopulate()
    +    {
    +        $_SERVER['HTTP_HOST']            = 'site1.com';
    +        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en-US';
    +        $_SERVER['HTTP_ACCEPT_ENCODING'] = 'gzip, deflate, br';
    +
    +        $options = [
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +            'headers'  => [
    +                'Host'            => 'www.foo.com',
    +                'Accept-Encoding' => '',
    +            ],
    +        ];
    +        $request = $this->getRequest($options);
    +        $request->get('example');
    +        // if headers for the request are defined we use them
    +        $this->assertNull($request->header('Accept-Language'));
    +        $this->assertSame('www.foo.com', $request->header('Host')->getValue());
    +        $this->assertSame('', $request->header('Accept-Encoding')->getValue());
    +    }
    +
    +    public function testHeaderContentLengthNotSharedBetweenRequests()
    +    {
    +        $options = [
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +        ];
    +        $request = $this->getRequest($options, false);
    +
    +        $request->post('example', [
    +            'form_params' => [
    +                'q' => 'keyword',
    +            ],
    +        ]);
    +        $request->get('example');
    +
    +        $this->assertNull($request->header('Content-Length'));
    +    }
    +
    +    /**
    +     * @backupGlobals enabled
    +     */
    +    public function testHeaderContentLengthNotSharedBetweenClients()
    +    {
    +        $_SERVER['HTTP_CONTENT_LENGTH'] = '10';
    +
    +        $options = [
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +        ];
    +        $request = $this->getRequest($options);
    +        $request->post('example', [
    +            'form_params' => [
    +                'q' => 'keyword',
    +            ],
    +        ]);
    +
    +        $request = $this->getRequest($options);
    +        $request->get('example');
    +
    +        $this->assertNull($request->header('Content-Length'));
    +    }
    +
    +    public function testOptionsDelay()
    +    {
    +        $options = [
    +            'delay'   => 2000,
    +            'headers' => ['fruit' => 'apple'],
    +        ];
    +        $request = $this->getRequest();
    +        $this->assertSame(0.0, $request->getDelay());
    +
    +        $request = $this->getRequest($options);
    +        $this->assertSame(2.0, $request->getDelay());
    +    }
    +
    +    public function testPatchSetsCorrectMethod()
    +    {
    +        $this->request->patch('http://example.com');
    +
    +        $this->assertSame('patch', $this->request->getMethod());
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options);
    +        $this->assertSame('PATCH', $options[CURLOPT_CUSTOMREQUEST]);
    +    }
    +
    +    public function testPostSetsCorrectMethod()
    +    {
    +        $this->request->post('http://example.com');
    +
    +        $this->assertSame('post', $this->request->getMethod());
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options);
    +        $this->assertSame('POST', $options[CURLOPT_CUSTOMREQUEST]);
    +    }
    +
    +    public function testPutSetsCorrectMethod()
    +    {
    +        $this->request->put('http://example.com');
    +
    +        $this->assertSame('put', $this->request->getMethod());
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options);
    +        $this->assertSame('PUT', $options[CURLOPT_CUSTOMREQUEST]);
    +    }
    +
    +    public function testCustomMethodSetsCorrectMethod()
    +    {
    +        $this->request->request('custom', 'http://example.com');
    +
    +        $this->assertSame('custom', $this->request->getMethod());
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options);
    +        $this->assertSame('CUSTOM', $options[CURLOPT_CUSTOMREQUEST]);
    +    }
    +
    +    public function testRequestMethodGetsSanitized()
    +    {
    +        $this->request->request('', 'http://example.com');
    +
    +        $this->assertSame('custom', $this->request->getMethod());
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options);
    +        $this->assertSame('CUSTOM', $options[CURLOPT_CUSTOMREQUEST]);
    +    }
    +
    +    public function testRequestSetsBasicCurlOptions()
    +    {
    +        $this->request->request('get', 'http://example.com');
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_URL, $options);
    +        $this->assertSame('http://example.com', $options[CURLOPT_URL]);
    +
    +        $this->assertArrayHasKey(CURLOPT_RETURNTRANSFER, $options);
    +        $this->assertTrue($options[CURLOPT_RETURNTRANSFER]);
    +
    +        $this->assertArrayHasKey(CURLOPT_HEADER, $options);
    +        $this->assertTrue($options[CURLOPT_HEADER]);
    +
    +        $this->assertArrayHasKey(CURLOPT_FRESH_CONNECT, $options);
    +        $this->assertTrue($options[CURLOPT_FRESH_CONNECT]);
    +
    +        $this->assertArrayHasKey(CURLOPT_TIMEOUT_MS, $options);
    +        $this->assertSame(0.0, $options[CURLOPT_TIMEOUT_MS]);
    +
    +        $this->assertArrayHasKey(CURLOPT_CONNECTTIMEOUT_MS, $options);
    +        $this->assertSame(150000.0, $options[CURLOPT_CONNECTTIMEOUT_MS]);
    +    }
    +
    +    public function testAuthBasicOption()
    +    {
    +        $this->request->request('get', 'http://example.com', [
    +            'auth' => [
    +                'username',
    +                'password',
    +            ],
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_USERPWD, $options);
    +        $this->assertSame('username:password', $options[CURLOPT_USERPWD]);
    +
    +        $this->assertArrayHasKey(CURLOPT_HTTPAUTH, $options);
    +        $this->assertSame(CURLAUTH_BASIC, $options[CURLOPT_HTTPAUTH]);
    +    }
    +
    +    public function testAuthBasicOptionExplicit()
    +    {
    +        $this->request->request('get', 'http://example.com', [
    +            'auth' => [
    +                'username',
    +                'password',
    +                'basic',
    +            ],
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_USERPWD, $options);
    +        $this->assertSame('username:password', $options[CURLOPT_USERPWD]);
    +
    +        $this->assertArrayHasKey(CURLOPT_HTTPAUTH, $options);
    +        $this->assertSame(CURLAUTH_BASIC, $options[CURLOPT_HTTPAUTH]);
    +    }
    +
    +    public function testAuthDigestOption()
    +    {
    +        $output = "HTTP/1.1 401 Unauthorized
    +		Server: ddos-guard
    +		Set-Cookie: __ddg1=z177j4mLtqzC07v0zviU; Domain=.site.ru; HttpOnly; Path=/; Expires=Wed, 07-Jul-2021 15:13:14 GMT
    +		WWW-Authenticate: Digest\x0d\x0a\x0d\x0aHTTP/1.1 200 OK
    +		Server: ddos-guard
    +		Connection: keep-alive
    +		Keep-Alive: timeout=60
    +		Set-Cookie: __ddg1=z177j4mLtqzC07v0zviU; Domain=.site.ru; HttpOnly; Path=/; Expires=Wed, 07-Jul-2021 15:13:14 GMT
    +		Date: Tue, 07 Jul 2020 15:13:14 GMT
    +		Expires: Thu, 19 Nov 1981 08:52:00 GMT
    +		Cache-Control: no-store, no-cache, must-revalidate
    +		Pragma: no-cache
    +		Set-Cookie: PHPSESSID=80pd3hlg38mvjnelpvokp9lad0; path=/
    +		Content-Type: application/xml; charset=utf-8
    +		Transfer-Encoding: chunked\x0d\x0a\x0d\x0aUpdate success! config";
    +
    +        $this->request->setOutput($output);
    +
    +        $response = $this->request->request('get', 'http://example.com', [
    +            'auth' => [
    +                'username',
    +                'password',
    +                'digest',
    +            ],
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertSame('Update success! config', $response->getBody());
    +        $this->assertSame(200, $response->getStatusCode());
    +
    +        $this->assertArrayHasKey(CURLOPT_USERPWD, $options);
    +        $this->assertSame('username:password', $options[CURLOPT_USERPWD]);
    +
    +        $this->assertArrayHasKey(CURLOPT_HTTPAUTH, $options);
    +        $this->assertSame(CURLAUTH_DIGEST, $options[CURLOPT_HTTPAUTH]);
    +    }
    +
    +    public function testSetAuthBasic()
    +    {
    +        $this->request->setAuth('username', 'password')->get('http://example.com');
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_USERPWD, $options);
    +        $this->assertSame('username:password', $options[CURLOPT_USERPWD]);
    +
    +        $this->assertArrayHasKey(CURLOPT_HTTPAUTH, $options);
    +        $this->assertSame(CURLAUTH_BASIC, $options[CURLOPT_HTTPAUTH]);
    +    }
    +
    +    public function testSetAuthDigest()
    +    {
    +        $output = "HTTP/1.1 401 Unauthorized
    +		Server: ddos-guard
    +		Set-Cookie: __ddg1=z177j4mLtqzC07v0zviU; Domain=.site.ru; HttpOnly; Path=/; Expires=Wed, 07-Jul-2021 15:13:14 GMT
    +		WWW-Authenticate: Digest\x0d\x0a\x0d\x0aHTTP/1.1 200 OK
    +		Server: ddos-guard
    +		Connection: keep-alive
    +		Keep-Alive: timeout=60
    +		Set-Cookie: __ddg1=z177j4mLtqzC07v0zviU; Domain=.site.ru; HttpOnly; Path=/; Expires=Wed, 07-Jul-2021 15:13:14 GMT
    +		Date: Tue, 07 Jul 2020 15:13:14 GMT
    +		Expires: Thu, 19 Nov 1981 08:52:00 GMT
    +		Cache-Control: no-store, no-cache, must-revalidate
    +		Pragma: no-cache
    +		Set-Cookie: PHPSESSID=80pd3hlg38mvjnelpvokp9lad0; path=/
    +		Content-Type: application/xml; charset=utf-8
    +		Transfer-Encoding: chunked\x0d\x0a\x0d\x0aUpdate success! config";
    +
    +        $this->request->setOutput($output);
    +
    +        $response = $this->request->setAuth('username', 'password', 'digest')->get('http://example.com');
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertSame('Update success! config', $response->getBody());
    +        $this->assertSame(200, $response->getStatusCode());
    +
    +        $this->assertArrayHasKey(CURLOPT_USERPWD, $options);
    +        $this->assertSame('username:password', $options[CURLOPT_USERPWD]);
    +
    +        $this->assertArrayHasKey(CURLOPT_HTTPAUTH, $options);
    +        $this->assertSame(CURLAUTH_DIGEST, $options[CURLOPT_HTTPAUTH]);
    +    }
    +
    +    public function testCertOption()
    +    {
    +        $file = __FILE__;
    +
    +        $this->request->request('get', 'http://example.com', [
    +            'cert' => $file,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_SSLCERT, $options);
    +        $this->assertSame($file, $options[CURLOPT_SSLCERT]);
    +    }
    +
    +    public function testCertOptionWithPassword()
    +    {
    +        $file = __FILE__;
    +
    +        $this->request->request('get', 'http://example.com', [
    +            'cert' => [
    +                $file,
    +                'password',
    +            ],
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_SSLCERT, $options);
    +        $this->assertSame($file, $options[CURLOPT_SSLCERT]);
    +
    +        $this->assertArrayHasKey(CURLOPT_SSLCERTPASSWD, $options);
    +        $this->assertSame('password', $options[CURLOPT_SSLCERTPASSWD]);
    +    }
    +
    +    public function testMissingCertOption()
    +    {
    +        $file = 'something_obviously_bogus';
    +        $this->expectException(HTTPException::class);
    +
    +        $this->request->request('get', 'http://example.com', [
    +            'cert' => $file,
    +        ]);
    +    }
    +
    +    public function testSSLVerification()
    +    {
    +        $file = __FILE__;
    +
    +        $this->request->request('get', 'http://example.com', [
    +            'verify'  => 'yes',
    +            'ssl_key' => $file,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_CAINFO, $options);
    +        $this->assertSame($file, $options[CURLOPT_CAINFO]);
    +
    +        $this->assertArrayHasKey(CURLOPT_SSL_VERIFYPEER, $options);
    +        $this->assertSame(1, $options[CURLOPT_SSL_VERIFYPEER]);
    +    }
    +
    +    public function testSSLWithBadKey()
    +    {
    +        $file = 'something_obviously_bogus';
    +        $this->expectException(HTTPException::class);
    +
    +        $this->request->request('get', 'http://example.com', [
    +            'verify'  => 'yes',
    +            'ssl_key' => $file,
    +        ]);
    +    }
    +
    +    public function testDebugOptionTrue()
    +    {
    +        $this->request->request('get', 'http://example.com', [
    +            'debug' => true,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_VERBOSE, $options);
    +        $this->assertSame(1, $options[CURLOPT_VERBOSE]);
    +
    +        $this->assertArrayHasKey(CURLOPT_STDERR, $options);
    +        $this->assertIsResource($options[CURLOPT_STDERR]);
    +    }
    +
    +    public function testDebugOptionFalse()
    +    {
    +        $this->request->request('get', 'http://example.com', [
    +            'debug' => false,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayNotHasKey(CURLOPT_VERBOSE, $options);
    +        $this->assertArrayNotHasKey(CURLOPT_STDERR, $options);
    +    }
    +
    +    public function testDebugOptionFile()
    +    {
    +        $file = SUPPORTPATH . 'Files/baker/banana.php';
    +
    +        $this->request->request('get', 'http://example.com', [
    +            'debug' => $file,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_VERBOSE, $options);
    +        $this->assertSame(1, $options[CURLOPT_VERBOSE]);
    +
    +        $this->assertArrayHasKey(CURLOPT_STDERR, $options);
    +        $this->assertIsResource($options[CURLOPT_STDERR]);
    +    }
    +
    +    public function testDecodeContent()
    +    {
    +        $this->request->setHeader('Accept-Encoding', 'cobol');
    +        $this->request->request('get', 'http://example.com', [
    +            'decode_content' => true,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_ENCODING, $options);
    +        $this->assertSame('cobol', $options[CURLOPT_ENCODING]);
    +    }
    +
    +    public function testDecodeContentWithoutAccept()
    +    {
    +        //      $this->request->setHeader('Accept-Encoding', 'cobol');
    +        $this->request->request('get', 'http://example.com', [
    +            'decode_content' => true,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_ENCODING, $options);
    +        $this->assertSame('', $options[CURLOPT_ENCODING]);
    +        $this->assertArrayHasKey(CURLOPT_HTTPHEADER, $options);
    +        $this->assertSame('Accept-Encoding', $options[CURLOPT_HTTPHEADER]);
    +    }
    +
    +    public function testAllowRedirectsOptionFalse()
    +    {
    +        $this->request->request('get', 'http://example.com', [
    +            'allow_redirects' => false,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_FOLLOWLOCATION, $options);
    +        $this->assertSame(0, $options[CURLOPT_FOLLOWLOCATION]);
    +
    +        $this->assertArrayNotHasKey(CURLOPT_MAXREDIRS, $options);
    +        $this->assertArrayNotHasKey(CURLOPT_REDIR_PROTOCOLS, $options);
    +    }
    +
    +    public function testAllowRedirectsOptionTrue()
    +    {
    +        $this->request->request('get', 'http://example.com', [
    +            'allow_redirects' => true,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_FOLLOWLOCATION, $options);
    +        $this->assertSame(1, $options[CURLOPT_FOLLOWLOCATION]);
    +
    +        $this->assertArrayHasKey(CURLOPT_MAXREDIRS, $options);
    +        $this->assertSame(5, $options[CURLOPT_MAXREDIRS]);
    +        $this->assertArrayHasKey(CURLOPT_REDIR_PROTOCOLS, $options);
    +        $this->assertSame(CURLPROTO_HTTP | CURLPROTO_HTTPS, $options[CURLOPT_REDIR_PROTOCOLS]);
    +    }
    +
    +    public function testAllowRedirectsOptionDefaults()
    +    {
    +        $this->request->request('get', 'http://example.com', [
    +            'allow_redirects' => true,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_FOLLOWLOCATION, $options);
    +        $this->assertSame(1, $options[CURLOPT_FOLLOWLOCATION]);
    +
    +        $this->assertArrayHasKey(CURLOPT_MAXREDIRS, $options);
    +        $this->assertArrayHasKey(CURLOPT_REDIR_PROTOCOLS, $options);
    +    }
    +
    +    public function testAllowRedirectsArray()
    +    {
    +        $this->request->request('get', 'http://example.com', [
    +            'allow_redirects' => ['max' => 2],
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_FOLLOWLOCATION, $options);
    +        $this->assertSame(1, $options[CURLOPT_FOLLOWLOCATION]);
    +
    +        $this->assertArrayHasKey(CURLOPT_MAXREDIRS, $options);
    +        $this->assertSame(2, $options[CURLOPT_MAXREDIRS]);
    +    }
    +
    +    public function testSendWithQuery()
    +    {
    +        $request = $this->getRequest([
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +            'query'    => [
    +                'name' => 'Henry',
    +                'd.t'  => 'value',
    +            ],
    +        ]);
    +
    +        $request->get('products');
    +
    +        $options = $request->curl_options;
    +
    +        $this->assertSame('http://www.foo.com/api/v1/products?name=Henry&d.t=value', $options[CURLOPT_URL]);
    +    }
    +
    +    public function testSendWithDelay()
    +    {
    +        $request = $this->getRequest([
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +            'delay'    => 1000,
    +        ]);
    +
    +        $request->get('products');
    +
    +        // we still need to check the code coverage to make sure this was done
    +        $this->assertSame(1.0, $request->getDelay());
    +    }
    +
    +    public function testSendContinued()
    +    {
    +        $request = $this->getRequest([
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +            'delay'    => 1000,
    +        ]);
    +
    +        $request->setOutput("HTTP/1.1 100 Continue\x0d\x0a\x0d\x0aHi there");
    +        $response = $request->get('answer');
    +        $this->assertSame('Hi there', $response->getBody());
    +    }
    +
    +    /**
    +     * See: https://github.com/codeigniter4/CodeIgniter4/issues/3261
    +     */
    +    public function testSendContinuedWithManyHeaders()
    +    {
    +        $request = $this->getRequest([
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +            'delay'    => 1000,
    +        ]);
    +
    +        $output = "HTTP/1.1 100 Continue
    +Server: ddos-guard
    +Set-Cookie: __ddg1=z177j4mLtqzC07v0zviU; Domain=.site.ru; HttpOnly; Path=/; Expires=Wed, 07-Jul-2021 15:13:14 GMT\x0d\x0a\x0d\x0aHTTP/1.1 200 OK
    +Server: ddos-guard
    +Connection: keep-alive
    +Keep-Alive: timeout=60
    +Set-Cookie: __ddg1=z177j4mLtqzC07v0zviU; Domain=.site.ru; HttpOnly; Path=/; Expires=Wed, 07-Jul-2021 15:13:14 GMT
    +Date: Tue, 07 Jul 2020 15:13:14 GMT
    +Expires: Thu, 19 Nov 1981 08:52:00 GMT
    +Cache-Control: no-store, no-cache, must-revalidate
    +Pragma: no-cache
    +Set-Cookie: PHPSESSID=80pd3hlg38mvjnelpvokp9lad0; path=/
    +Content-Type: application/xml; charset=utf-8
    +Transfer-Encoding: chunked\x0d\x0a\x0d\x0aUpdate success! config";
    +
    +        $request->setOutput($output);
    +        $response = $request->get('answer');
    +
    +        $this->assertSame('Update success! config', $response->getBody());
    +
    +        $responseHeaderKeys = [
    +            'Cache-control',
    +            'Content-Type',
    +            'Server',
    +            'Connection',
    +            'Keep-Alive',
    +            'Set-Cookie',
    +            'Date',
    +            'Expires',
    +            'Pragma',
    +            'Transfer-Encoding',
    +        ];
    +        $this->assertSame($responseHeaderKeys, array_keys($response->headers()));
    +
    +        $this->assertSame(200, $response->getStatusCode());
    +    }
    +
    +    public function testSplitResponse()
    +    {
    +        $request = $this->getRequest([
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +            'delay'    => 1000,
    +        ]);
    +
    +        $request->setOutput("Accept: text/html\x0d\x0a\x0d\x0aHi there");
    +        $response = $request->get('answer');
    +        $this->assertSame('Hi there', $response->getBody());
    +    }
    +
    +    public function testApplyBody()
    +    {
    +        $request = $this->getRequest([
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +            'delay'    => 1000,
    +        ]);
    +
    +        $request->setBody('name=George');
    +        $request->setOutput('Hi there');
    +        $response = $request->post('answer');
    +
    +        $this->assertSame('Hi there', $response->getBody());
    +        $this->assertSame('name=George', $request->curl_options[CURLOPT_POSTFIELDS]);
    +    }
    +
    +    public function testResponseHeaders()
    +    {
    +        $request = $this->getRequest([
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +            'delay'    => 1000,
    +        ]);
    +
    +        $request->setOutput("HTTP/2.0 234 Ohoh\x0d\x0aAccept: text/html\x0d\x0a\x0d\x0aHi there");
    +        $response = $request->get('bogus');
    +
    +        $this->assertSame('2.0', $response->getProtocolVersion());
    +        $this->assertSame(234, $response->getStatusCode());
    +    }
    +
    +    public function testResponseHeadersShortProtocol()
    +    {
    +        $request = $this->getRequest([
    +            'base_uri' => 'http://www.foo.com/api/v1/',
    +            'delay'    => 1000,
    +        ]);
    +
    +        $request->setOutput("HTTP/2 235 Ohoh\x0d\x0aAccept: text/html\x0d\x0a\x0d\x0aHi there shortie");
    +        $response = $request->get('bogus');
    +
    +        $this->assertSame('2.0', $response->getProtocolVersion());
    +        $this->assertSame(235, $response->getStatusCode());
    +    }
    +
    +    public function testPostFormEncoded()
    +    {
    +        $params = [
    +            'foo' => 'bar',
    +            'baz' => [
    +                'hi',
    +                'there',
    +            ],
    +        ];
    +        $this->request->request('POST', '/post', [
    +            'form_params' => $params,
    +        ]);
    +
    +        $this->assertSame('post', $this->request->getMethod());
    +
    +        $options = $this->request->curl_options;
    +
    +        $expected = http_build_query($params);
    +        $this->assertArrayHasKey(CURLOPT_POSTFIELDS, $options);
    +        $this->assertSame($expected, $options[CURLOPT_POSTFIELDS]);
    +    }
    +
    +    public function testPostFormMultipart()
    +    {
    +        $params = [
    +            'foo' => 'bar',
    +            'baz' => [
    +                'hi',
    +                'there',
    +            ],
    +            'afile' => new CURLFile(__FILE__),
    +        ];
    +        $this->request->request('POST', '/post', [
    +            'multipart' => $params,
    +        ]);
    +
    +        $this->assertSame('post', $this->request->getMethod());
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_POSTFIELDS, $options);
    +        $this->assertSame($params, $options[CURLOPT_POSTFIELDS]);
    +    }
    +
    +    public function testSetForm()
    +    {
    +        $params = [
    +            'foo' => 'bar',
    +            'baz' => [
    +                'hi',
    +                'there',
    +            ],
    +        ];
    +
    +        $this->request->setForm($params)->post('/post');
    +
    +        $this->assertSame(
    +            http_build_query($params),
    +            $this->request->curl_options[CURLOPT_POSTFIELDS]
    +        );
    +
    +        $params['afile'] = new CURLFile(__FILE__);
    +
    +        $this->request->setForm($params, true)->post('/post');
    +
    +        $this->assertSame(
    +            $params,
    +            $this->request->curl_options[CURLOPT_POSTFIELDS]
    +        );
    +    }
    +
    +    public function testJSONData()
    +    {
    +        $params = [
    +            'foo' => 'bar',
    +            'baz' => [
    +                'hi',
    +                'there',
    +            ],
    +        ];
    +        $this->request->request('POST', '/post', [
    +            'json' => $params,
    +        ]);
    +
    +        $this->assertSame('post', $this->request->getMethod());
    +
    +        $expected = json_encode($params);
    +        $this->assertSame($expected, $this->request->getBody());
    +    }
    +
    +    public function testSetJSON()
    +    {
    +        $params = [
    +            'foo' => 'bar',
    +            'baz' => [
    +                'hi',
    +                'there',
    +            ],
    +        ];
    +        $this->request->setJSON($params)->post('/post');
    +
    +        $this->assertSame(json_encode($params), $this->request->getBody());
    +        $this->assertSame(
    +            'Content-Type: application/json',
    +            $this->request->curl_options[CURLOPT_HTTPHEADER][0]
    +        );
    +    }
    +
    +    public function testHTTPv1()
    +    {
    +        $this->request->request('POST', '/post', [
    +            'version' => 1.0,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_HTTP_VERSION, $options);
    +        $this->assertSame(CURL_HTTP_VERSION_1_0, $options[CURLOPT_HTTP_VERSION]);
    +    }
    +
    +    public function testHTTPv11()
    +    {
    +        $this->request->request('POST', '/post', [
    +            'version' => 1.1,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_HTTP_VERSION, $options);
    +        $this->assertSame(CURL_HTTP_VERSION_1_1, $options[CURLOPT_HTTP_VERSION]);
    +    }
    +
    +    public function testCookieOption()
    +    {
    +        $holder = SUPPORTPATH . 'HTTP/Files/CookiesHolder.txt';
    +        $this->request->request('POST', '/post', [
    +            'cookie' => $holder,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_COOKIEJAR, $options);
    +        $this->assertSame($holder, $options[CURLOPT_COOKIEJAR]);
    +        $this->assertArrayHasKey(CURLOPT_COOKIEFILE, $options);
    +        $this->assertSame($holder, $options[CURLOPT_COOKIEFILE]);
    +    }
    +
    +    public function testUserAgentOption()
    +    {
    +        $agent = 'CodeIgniter Framework';
    +
    +        $this->request->request('POST', '/post', [
    +            'user_agent' => $agent,
    +        ]);
    +
    +        $options = $this->request->curl_options;
    +
    +        $this->assertArrayHasKey(CURLOPT_USERAGENT, $options);
    +        $this->assertSame($agent, $options[CURLOPT_USERAGENT]);
    +    }
    +}
    diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php
    index 7237323f0be4..2d71572f3c94 100644
    --- a/tests/system/HTTP/CURLRequestTest.php
    +++ b/tests/system/HTTP/CURLRequestTest.php
    @@ -36,11 +36,11 @@ protected function setUp(): void
             $this->request = $this->getRequest();
         }
     
    -    protected function getRequest(array $options = [], $shareOptions = true)
    +    protected function getRequest(array $options = [])
         {
             $uri                          = isset($options['base_uri']) ? new URI($options['base_uri']) : new URI();
             $app                          = new App();
    -        $app->CURLRequestShareOptions = $shareOptions;
    +        $app->CURLRequestShareOptions = true;
     
             return new MockCURLRequest(($app), $uri, new Response($app), $options);
         }
    @@ -199,23 +199,6 @@ public function testOptionsHeadersNotUsingPopulate()
             $this->assertSame('', $request->header('Accept-Encoding')->getValue());
         }
     
    -    public function testHeaderContentLengthNotSharedBetweenRequestsWhenSharedOptionsFalse()
    -    {
    -        $options = [
    -            'base_uri' => 'http://www.foo.com/api/v1/',
    -        ];
    -        $request = $this->getRequest($options, false);
    -
    -        $request->post('example', [
    -            'form_params' => [
    -                'q' => 'keyword',
    -            ],
    -        ]);
    -        $request->get('example');
    -
    -        $this->assertNull($request->header('Content-Length'));
    -    }
    -
         /**
          * @backupGlobals enabled
          */
    
    From 9e88ec4a41e1930c83970e6c0d7ca02763b8d4da Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 29 Oct 2021 18:56:06 +0900
    Subject: [PATCH 393/490] test: add tests for sharing options
    
    ---
     .../HTTP/CURLRequestDoNotShareOptionsTest.php | 21 +++++++++++++++++++
     tests/system/HTTP/CURLRequestTest.php         | 21 +++++++++++++++++++
     2 files changed, 42 insertions(+)
    
    diff --git a/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    index d40356118f79..ae6aef2ad74d 100644
    --- a/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    +++ b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    @@ -199,6 +199,27 @@ public function testOptionsHeadersNotUsingPopulate()
             $this->assertSame('', $request->header('Accept-Encoding')->getValue());
         }
     
    +    public function testDefaultOptionsAreSharedBetweenRequests()
    +    {
    +        $options = [
    +            'form_params' => ['studio' => 1],
    +            'user_agent'  => 'CodeIgniter Framework v4',
    +        ];
    +        $request = $this->getRequest($options);
    +
    +        $request->request('POST', 'https://realestate1.example.com');
    +
    +        $this->assertSame('https://realestate1.example.com', $request->curl_options[CURLOPT_URL]);
    +        $this->assertSame('studio=1', $request->curl_options[CURLOPT_POSTFIELDS]);
    +        $this->assertSame('CodeIgniter Framework v4', $request->curl_options[CURLOPT_USERAGENT]);
    +
    +        $request->request('POST', 'https://realestate2.example.com');
    +
    +        $this->assertSame('https://realestate2.example.com', $request->curl_options[CURLOPT_URL]);
    +        $this->assertSame('studio=1', $request->curl_options[CURLOPT_POSTFIELDS]);
    +        $this->assertSame('CodeIgniter Framework v4', $request->curl_options[CURLOPT_USERAGENT]);
    +    }
    +
         public function testHeaderContentLengthNotSharedBetweenRequests()
         {
             $options = [
    diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php
    index 2d71572f3c94..de7ef08b59fc 100644
    --- a/tests/system/HTTP/CURLRequestTest.php
    +++ b/tests/system/HTTP/CURLRequestTest.php
    @@ -199,6 +199,27 @@ public function testOptionsHeadersNotUsingPopulate()
             $this->assertSame('', $request->header('Accept-Encoding')->getValue());
         }
     
    +    public function testOptionsAreSharedBetweenRequests()
    +    {
    +        $options = [
    +            'form_params' => ['studio' => 1],
    +            'user_agent'  => 'CodeIgniter Framework v4',
    +        ];
    +        $request = $this->getRequest($options);
    +
    +        $request->request('POST', 'https://realestate1.example.com');
    +
    +        $this->assertSame('https://realestate1.example.com', $request->curl_options[CURLOPT_URL]);
    +        $this->assertSame('studio=1', $request->curl_options[CURLOPT_POSTFIELDS]);
    +        $this->assertSame('CodeIgniter Framework v4', $request->curl_options[CURLOPT_USERAGENT]);
    +
    +        $request->request('POST', 'https://realestate2.example.com');
    +
    +        $this->assertSame('https://realestate2.example.com', $request->curl_options[CURLOPT_URL]);
    +        $this->assertSame('studio=1', $request->curl_options[CURLOPT_POSTFIELDS]);
    +        $this->assertSame('CodeIgniter Framework v4', $request->curl_options[CURLOPT_USERAGENT]);
    +    }
    +
         /**
          * @backupGlobals enabled
          */
    
    From b648d2d724a0f313b664fd515f0023911381403b Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA" 
    Date: Fri, 29 Oct 2021 18:18:25 +0800
    Subject: [PATCH 394/490] Cleanup php-cs-fixer config files
    
    ---
     .no-header.php-cs-fixer.dist.php | 21 +--------------------
     .php-cs-fixer.dist.php           | 21 +--------------------
     2 files changed, 2 insertions(+), 40 deletions(-)
    
    diff --git a/.no-header.php-cs-fixer.dist.php b/.no-header.php-cs-fixer.dist.php
    index a3030aa98f58..12acddb81087 100644
    --- a/.no-header.php-cs-fixer.dist.php
    +++ b/.no-header.php-cs-fixer.dist.php
    @@ -27,26 +27,7 @@
         ])
         ->notName('#Logger\.php$#');
     
    -$overrides = [
    -    // @TODO Remove once these are live in coding-standard
    -    'assign_null_coalescing_to_coalesce_equal' => false, // requires 7.4+
    -    'class_attributes_separation'              => [
    -        'elements' => [
    -            'const'        => 'none',
    -            'property'     => 'none',
    -            'method'       => 'one',
    -            'trait_import' => 'none',
    -        ],
    -    ],
    -    'control_structure_continuation_position' => ['position' => 'same_line'],
    -    'empty_loop_condition'                    => ['style' => 'while'],
    -    'integer_literal_case'                    => true,
    -    'modernize_strpos'                        => false, // requires 8.0+
    -    'no_alternative_syntax'                   => ['fix_non_monolithic_code' => false],
    -    'no_space_around_double_colon'            => true,
    -    'octal_notation'                          => false, // requires 8.1+
    -    'string_length_to_empty'                  => true,
    -];
    +$overrides = [];
     
     $options = [
         'cacheFile'    => 'build/.no-header.php-cs-fixer.cache',
    diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
    index 743062d44a88..3c9e05fe3b74 100644
    --- a/.php-cs-fixer.dist.php
    +++ b/.php-cs-fixer.dist.php
    @@ -34,26 +34,7 @@
             __DIR__ . '/spark',
         ]);
     
    -$overrides = [
    -    // @TODO Remove once these are live in coding-standard
    -    'assign_null_coalescing_to_coalesce_equal' => false, // requires 7.4+
    -    'class_attributes_separation'              => [
    -        'elements' => [
    -            'const'        => 'none',
    -            'property'     => 'none',
    -            'method'       => 'one',
    -            'trait_import' => 'none',
    -        ],
    -    ],
    -    'control_structure_continuation_position' => ['position' => 'same_line'],
    -    'empty_loop_condition'                    => ['style' => 'while'],
    -    'integer_literal_case'                    => true,
    -    'modernize_strpos'                        => false, // requires 8.0+
    -    'no_alternative_syntax'                   => ['fix_non_monolithic_code' => false],
    -    'no_space_around_double_colon'            => true,
    -    'octal_notation'                          => false, // requires 8.1+
    -    'string_length_to_empty'                  => true,
    -];
    +$overrides = [];
     
     $options = [
         'cacheFile'    => 'build/.php-cs-fixer.cache',
    
    From 3ac80c37d8482716c7189bcca69bc43d1dc4823d Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA" 
    Date: Fri, 29 Oct 2021 18:23:05 +0800
    Subject: [PATCH 395/490] Run `builds` thru CS lint
    
    ---
     .no-header.php-cs-fixer.dist.php |   5 +-
     admin/starter/builds             | 236 ++++++++++++++-----------------
     2 files changed, 111 insertions(+), 130 deletions(-)
    
    diff --git a/.no-header.php-cs-fixer.dist.php b/.no-header.php-cs-fixer.dist.php
    index a3030aa98f58..f3b427daec4a 100644
    --- a/.no-header.php-cs-fixer.dist.php
    +++ b/.no-header.php-cs-fixer.dist.php
    @@ -25,7 +25,10 @@
             __DIR__ . '/app',
             __DIR__ . '/public',
         ])
    -    ->notName('#Logger\.php$#');
    +    ->notName('#Logger\.php$#')
    +    ->append([
    +        __DIR__ . '/admin/starter/builds',
    +    ]);
     
     $overrides = [
         // @TODO Remove once these are live in coding-standard
    diff --git a/admin/starter/builds b/admin/starter/builds
    index 268e7a819c22..1ac374f57a75 100755
    --- a/admin/starter/builds
    +++ b/admin/starter/builds
    @@ -15,149 +15,127 @@ define('GITHUB_URL', 'https://github.com/codeigniter4/codeigniter4');
      */
     
     // Determine the requested stability
    -if (empty($argv[1]) || ! in_array($argv[1], ['release', 'development']))
    -{
    -	echo 'Usage: php builds [release|development]' . PHP_EOL;
    -	exit;
    +if (empty($argv[1]) || ! in_array($argv[1], ['release', 'development'], true)) {
    +    echo 'Usage: php builds [release|development]' . PHP_EOL;
    +
    +    exit;
     }
     
    -$dev = $argv[1] == 'development';
    +$dev = $argv[1] === 'development';
    +
     $modified = [];
     
    -/* Locate each file and update it for the requested stability */
    +// Locate each file and update it for the requested stability
     
     // Composer.json
     $file = __DIR__ . DIRECTORY_SEPARATOR . 'composer.json';
     
    -if (is_file($file))
    -{
    -	// Make sure we can read it
    -	if ($contents = file_get_contents($file))
    -	{
    -		if ($array = json_decode($contents, true))
    -		{
    -			// Development
    -			if ($dev)
    -			{
    -				// Set 'minimum-stability'
    -				$array['minimum-stability'] = 'dev';
    -				$array['prefer-stable']     = true;
    -
    -				// Make sure the repo is configured
    -				if (! isset($array['repositories']))
    -				{
    -					$array['repositories'] = [];
    -				}
    -
    -				// Check for the CodeIgniter repo
    -				$found = false;
    -				foreach ($array['repositories'] as $repository)
    -				{
    -					if ($repository['url'] == GITHUB_URL)
    -					{
    -						$found = true;
    -						break;
    -					}
    -				}
    -
    -				// Add the repo if it was not found
    -				if (! $found)
    -				{
    -					$array['repositories'][] = [
    -						'type' => 'vcs',
    -						'url'  => GITHUB_URL,
    -					];
    -				}
    -
    -				// Define the "require"
    -				$array['require']['codeigniter4/codeigniter4'] = 'dev-develop';
    -				unset($array['require']['codeigniter4/framework']);
    -			}
    -
    -			// Release
    -			else
    -			{
    -				// Clear 'minimum-stability'
    -				unset($array['minimum-stability']);
    -
    -				// If the repo is configured then clear it
    -				if (isset($array['repositories']))
    -				{
    -					// Check for the CodeIgniter repo
    -					foreach ($array['repositories'] as $i => $repository)
    -					{
    -						if ($repository['url'] == GITHUB_URL)
    -						{
    -							unset($array['repositories'][$i]);
    -							break;
    -						}
    -					}
    -					if (empty($array['repositories']))
    -					{
    -						unset($array['repositories']);
    -					}
    -				}
    -
    -				// Define the "require"
    -				$array['require']['codeigniter4/framework'] = LATEST_RELEASE;
    -				unset($array['require']['codeigniter4/codeigniter4']);
    -			}
    -
    -			// Write out a new composer.json
    -			file_put_contents($file, json_encode($array, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES) . PHP_EOL);
    -			$modified[] = $file;
    -		}
    -		else
    -		{
    -			echo 'Warning: Unable to decode composer.json! Skipping...' . PHP_EOL;
    -		}
    -	}
    -	else
    -	{
    -		echo 'Warning: Unable to read composer.json! Skipping...' . PHP_EOL;
    -	}
    +if (is_file($file)) {
    +    // Make sure we can read it
    +    if ($contents = file_get_contents($file)) {
    +        if ($array = json_decode($contents, true)) {
    +            // Development
    +            if ($dev) {
    +                // Set 'minimum-stability'
    +                $array['minimum-stability'] = 'dev';
    +                $array['prefer-stable']     = true;
    +
    +                // Make sure the repo is configured
    +                if (! isset($array['repositories'])) {
    +                    $array['repositories'] = [];
    +                }
    +
    +                // Check for the CodeIgniter repo
    +                $found = false;
    +
    +                foreach ($array['repositories'] as $repository) {
    +                    if ($repository['url'] === GITHUB_URL) {
    +                        $found = true;
    +                        break;
    +                    }
    +                }
    +
    +                // Add the repo if it was not found
    +                if (! $found) {
    +                    $array['repositories'][] = [
    +                        'type' => 'vcs',
    +                        'url'  => GITHUB_URL,
    +                    ];
    +                }
    +
    +                // Define the "require"
    +                $array['require']['codeigniter4/codeigniter4'] = 'dev-develop';
    +                unset($array['require']['codeigniter4/framework']);
    +            }
    +
    +            // Release
    +            else {
    +                // Clear 'minimum-stability'
    +                unset($array['minimum-stability']);
    +
    +                // If the repo is configured then clear it
    +                if (isset($array['repositories'])) {
    +                    // Check for the CodeIgniter repo
    +                    foreach ($array['repositories'] as $i => $repository) {
    +                        if ($repository['url'] === GITHUB_URL) {
    +                            unset($array['repositories'][$i]);
    +                            break;
    +                        }
    +                    }
    +                    if (empty($array['repositories'])) {
    +                        unset($array['repositories']);
    +                    }
    +                }
    +
    +                // Define the "require"
    +                $array['require']['codeigniter4/framework'] = LATEST_RELEASE;
    +                unset($array['require']['codeigniter4/codeigniter4']);
    +            }
    +
    +            // Write out a new composer.json
    +            file_put_contents($file, json_encode($array, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
    +            $modified[] = $file;
    +        } else {
    +            echo 'Warning: Unable to decode composer.json! Skipping...' . PHP_EOL;
    +        }
    +    } else {
    +        echo 'Warning: Unable to read composer.json! Skipping...' . PHP_EOL;
    +    }
     }
     
     // Paths config and PHPUnit XMLs
     $files = [
    -	__DIR__ . DIRECTORY_SEPARATOR . 'app/Config/Paths.php',
    -	__DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml.dist',
    -	__DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml',
    +    __DIR__ . DIRECTORY_SEPARATOR . 'app/Config/Paths.php',
    +    __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml.dist',
    +    __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml',
     ];
     
    -foreach ($files as $file)
    -{
    -	if (is_file($file))
    -	{
    -		$contents = file_get_contents($file);
    -
    -		// Development
    -		if ($dev)
    -		{
    -			$contents = str_replace('vendor/codeigniter4/framework', 'vendor/codeigniter4/codeigniter4', $contents);
    -		}
    -
    -		// Release
    -		else
    -		{
    -			$contents = str_replace('vendor/codeigniter4/codeigniter4', 'vendor/codeigniter4/framework', $contents);
    -		}
    -
    -		file_put_contents($file, $contents);
    -		$modified[] = $file;
    -	}
    -}
    +foreach ($files as $file) {
    +    if (is_file($file)) {
    +        $contents = file_get_contents($file);
    +
    +        // Development
    +        if ($dev) {
    +            $contents = str_replace('vendor/codeigniter4/framework', 'vendor/codeigniter4/codeigniter4', $contents);
    +        }
     
    -if (empty($modified))
    -{
    -	echo 'No files modified' . PHP_EOL;
    +        // Release
    +        else {
    +            $contents = str_replace('vendor/codeigniter4/codeigniter4', 'vendor/codeigniter4/framework', $contents);
    +        }
    +
    +        file_put_contents($file, $contents);
    +        $modified[] = $file;
    +    }
     }
    -else
    -{
    -	echo 'The following files were modified:' . PHP_EOL;
    -	foreach ($modified as $file)
    -	{
    -		echo " * {$file}" . PHP_EOL;
    -	}
    -	echo 'Run `composer update` to sync changes with your vendor folder' . PHP_EOL;
    +
    +if (empty($modified)) {
    +    echo 'No files modified' . PHP_EOL;
    +} else {
    +    echo 'The following files were modified:' . PHP_EOL;
    +
    +    foreach ($modified as $file) {
    +        echo " * {$file}" . PHP_EOL;
    +    }
    +    echo 'Run `composer update` to sync changes with your vendor folder' . PHP_EOL;
     }
    
    From ef47af446062befef5181eada32be3d0e7c13b26 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA" 
    Date: Fri, 29 Oct 2021 18:26:03 +0800
    Subject: [PATCH 396/490] Remove unnecessary comments
    
    ---
     admin/starter/builds | 31 +++++++------------------------
     1 file changed, 7 insertions(+), 24 deletions(-)
    
    diff --git a/admin/starter/builds b/admin/starter/builds
    index 1ac374f57a75..3ef9fd3d6ec7 100755
    --- a/admin/starter/builds
    +++ b/admin/starter/builds
    @@ -27,25 +27,19 @@ $modified = [];
     
     // Locate each file and update it for the requested stability
     
    -// Composer.json
     $file = __DIR__ . DIRECTORY_SEPARATOR . 'composer.json';
     
     if (is_file($file)) {
    -    // Make sure we can read it
         if ($contents = file_get_contents($file)) {
             if ($array = json_decode($contents, true)) {
    -            // Development
                 if ($dev) {
    -                // Set 'minimum-stability'
                     $array['minimum-stability'] = 'dev';
                     $array['prefer-stable']     = true;
     
    -                // Make sure the repo is configured
                     if (! isset($array['repositories'])) {
                         $array['repositories'] = [];
                     }
     
    -                // Check for the CodeIgniter repo
                     $found = false;
     
                     foreach ($array['repositories'] as $repository) {
    @@ -55,7 +49,6 @@ if (is_file($file)) {
                         }
                     }
     
    -                // Add the repo if it was not found
                     if (! $found) {
                         $array['repositories'][] = [
                             'type' => 'vcs',
    @@ -63,37 +56,30 @@ if (is_file($file)) {
                         ];
                     }
     
    -                // Define the "require"
                     $array['require']['codeigniter4/codeigniter4'] = 'dev-develop';
                     unset($array['require']['codeigniter4/framework']);
    -            }
    -
    -            // Release
    -            else {
    -                // Clear 'minimum-stability'
    +            } else {
                     unset($array['minimum-stability']);
     
    -                // If the repo is configured then clear it
                     if (isset($array['repositories'])) {
    -                    // Check for the CodeIgniter repo
                         foreach ($array['repositories'] as $i => $repository) {
                             if ($repository['url'] === GITHUB_URL) {
                                 unset($array['repositories'][$i]);
                                 break;
                             }
                         }
    +
                         if (empty($array['repositories'])) {
                             unset($array['repositories']);
                         }
                     }
     
    -                // Define the "require"
                     $array['require']['codeigniter4/framework'] = LATEST_RELEASE;
                     unset($array['require']['codeigniter4/codeigniter4']);
                 }
     
    -            // Write out a new composer.json
                 file_put_contents($file, json_encode($array, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
    +
                 $modified[] = $file;
             } else {
                 echo 'Warning: Unable to decode composer.json! Skipping...' . PHP_EOL;
    @@ -103,7 +89,6 @@ if (is_file($file)) {
         }
     }
     
    -// Paths config and PHPUnit XMLs
     $files = [
         __DIR__ . DIRECTORY_SEPARATOR . 'app/Config/Paths.php',
         __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml.dist',
    @@ -114,17 +99,14 @@ foreach ($files as $file) {
         if (is_file($file)) {
             $contents = file_get_contents($file);
     
    -        // Development
             if ($dev) {
                 $contents = str_replace('vendor/codeigniter4/framework', 'vendor/codeigniter4/codeigniter4', $contents);
    -        }
    -
    -        // Release
    -        else {
    +        } else {
                 $contents = str_replace('vendor/codeigniter4/codeigniter4', 'vendor/codeigniter4/framework', $contents);
             }
     
             file_put_contents($file, $contents);
    +
             $modified[] = $file;
         }
     }
    @@ -137,5 +119,6 @@ if (empty($modified)) {
         foreach ($modified as $file) {
             echo " * {$file}" . PHP_EOL;
         }
    -    echo 'Run `composer update` to sync changes with your vendor folder' . PHP_EOL;
    +
    +    echo 'Run `composer update` to sync changes with your vendor folder.' . PHP_EOL;
     }
    
    From 1a048f9ba3dd723efd0809b677175e03e83bb558 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA" 
    Date: Fri, 29 Oct 2021 18:33:02 +0800
    Subject: [PATCH 397/490] Other optimizations
    
    ---
     admin/starter/builds | 17 +++++++++--------
     1 file changed, 9 insertions(+), 8 deletions(-)
    
    diff --git a/admin/starter/builds b/admin/starter/builds
    index 3ef9fd3d6ec7..4a3fe3d53139 100755
    --- a/admin/starter/builds
    +++ b/admin/starter/builds
    @@ -30,15 +30,16 @@ $modified = [];
     $file = __DIR__ . DIRECTORY_SEPARATOR . 'composer.json';
     
     if (is_file($file)) {
    -    if ($contents = file_get_contents($file)) {
    -        if ($array = json_decode($contents, true)) {
    +    $contents = file_get_contents($file);
    +
    +    if ((string) $contents !== '') {
    +        $array = json_decode($contents, true);
    +
    +        if (is_array($array)) {
                 if ($dev) {
                     $array['minimum-stability'] = 'dev';
                     $array['prefer-stable']     = true;
    -
    -                if (! isset($array['repositories'])) {
    -                    $array['repositories'] = [];
    -                }
    +                $array['repositories']      = $array['repositories'] ?? [];
     
                     $found = false;
     
    @@ -111,8 +112,8 @@ foreach ($files as $file) {
         }
     }
     
    -if (empty($modified)) {
    -    echo 'No files modified' . PHP_EOL;
    +if ($modified !== []) {
    +    echo 'No files modified.' . PHP_EOL;
     } else {
         echo 'The following files were modified:' . PHP_EOL;
     
    
    From c28ae89840bfafd605ac2314fc9c1a0fd7ead164 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA" 
    Date: Fri, 29 Oct 2021 18:45:33 +0800
    Subject: [PATCH 398/490] Disable blank issues
    
    ---
     .github/ISSUE_TEMPLATE/config.yml          |  9 +++++++++
     .github/ISSUE_TEMPLATE/support-question.md | 11 -----------
     2 files changed, 9 insertions(+), 11 deletions(-)
     create mode 100644 .github/ISSUE_TEMPLATE/config.yml
     delete mode 100644 .github/ISSUE_TEMPLATE/support-question.md
    
    diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
    new file mode 100644
    index 000000000000..69bb11b44fe8
    --- /dev/null
    +++ b/.github/ISSUE_TEMPLATE/config.yml
    @@ -0,0 +1,9 @@
    +blank_issues_enabled: false
    +contact_links:
    +  - name: CodeIgniter Forum
    +    url: https://forum.codeigniter.com
    +    about: Please ask your support questions in the forums. Thanks!
    +
    +  - name: CodeIgniter Slack channel
    +    url: https://codeigniterchat.slack.com
    +    about: Engage with other members of the community in our Slack channel.
    diff --git a/.github/ISSUE_TEMPLATE/support-question.md b/.github/ISSUE_TEMPLATE/support-question.md
    deleted file mode 100644
    index 390991ac99d0..000000000000
    --- a/.github/ISSUE_TEMPLATE/support-question.md
    +++ /dev/null
    @@ -1,11 +0,0 @@
    ----
    -name: Support question
    -about: How to ask a support question
    -title: ''
    -labels: ''
    -assignees: ''
    -
    ----
    -
    -Please ask support questions on our [forum](https://forum.codeigniter.com/forum-30.html).
    -We use github issues to track bugs and planned work.
    
    From d7364cdd1d9fa54379adc4682f433cb3d36fabcb Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA" 
    Date: Fri, 29 Oct 2021 19:22:00 +0800
    Subject: [PATCH 399/490] Speed up `CommonSingleServiceTest`
    
    ---
     tests/system/CommonSingleServiceTest.php | 58 +++++++++++++++++-------
     1 file changed, 41 insertions(+), 17 deletions(-)
    
    diff --git a/tests/system/CommonSingleServiceTest.php b/tests/system/CommonSingleServiceTest.php
    index ede9d9b0445e..bc2f4a1f60c8 100644
    --- a/tests/system/CommonSingleServiceTest.php
    +++ b/tests/system/CommonSingleServiceTest.php
    @@ -11,6 +11,7 @@
     
     namespace CodeIgniter;
     
    +use CodeIgniter\Autoloader\FileLocator;
     use CodeIgniter\Config\Services;
     use CodeIgniter\Test\CIUnitTestCase;
     use CodeIgniter\Test\Mock\MockSecurity;
    @@ -42,6 +43,17 @@ public function testSingleServiceWithNoParamsSupplied(string $service): void
          */
         public function testSingleServiceWithAtLeastOneParamSupplied(string $service): void
         {
    +        if ($service === 'commands') {
    +            $locator = $this->getMockBuilder(FileLocator::class)
    +                ->setConstructorArgs([Services::autoloader()])
    +                ->onlyMethods(['listFiles'])
    +                ->getMock();
    +
    +            // `Commands::discoverCommand()` is an expensive operation
    +            $locator->method('listFiles')->with('Commands/')->willReturn([]);
    +            Services::injectMock('locator', $locator);
    +        }
    +
             $params = [];
             $method = new ReflectionMethod(Services::class, $service);
     
    @@ -52,6 +64,10 @@ public function testSingleServiceWithAtLeastOneParamSupplied(string $service): v
     
             $this->assertSame(get_class($service1), get_class($service2));
             $this->assertNotSame($service1, $service2);
    +
    +        if ($service === 'commands') {
    +            $this->resetServices();
    +        }
         }
     
         public function testSingleServiceWithAllParamsSupplied(): void
    @@ -76,25 +92,33 @@ public function testSingleServiceWithGibberishGiven(): void
     
         public static function serviceNamesProvider(): iterable
         {
    -        $methods = (new ReflectionClass(Services::class))->getMethods(ReflectionMethod::IS_PUBLIC);
    -
    -        foreach ($methods as $method) {
    -            $name = $method->getName();
    -            $excl = [
    -                '__callStatic',
    -                'serviceExists',
    -                'reset',
    -                'resetSingle',
    -                'injectMock',
    -                'encrypter', // Encrypter needs a starter key
    -                'session', // Headers already sent
    -            ];
    -
    -            if (in_array($name, $excl, true)) {
    -                continue;
    +        static $services = [];
    +        static $excl     = [
    +            '__callStatic',
    +            'serviceExists',
    +            'reset',
    +            'resetSingle',
    +            'injectMock',
    +            'encrypter', // Encrypter needs a starter key
    +            'session', // Headers already sent
    +        ];
    +
    +        if ($services === []) {
    +            $methods = (new ReflectionClass(Services::class))->getMethods(ReflectionMethod::IS_PUBLIC);
    +
    +            foreach ($methods as $method) {
    +                $name = $method->getName();
    +
    +                if (in_array($name, $excl, true)) {
    +                    continue;
    +                }
    +
    +                $services[$name] = [$name];
                 }
     
    -            yield $name => [$name];
    +            ksort($services);
             }
    +
    +        yield from $services;
         }
     }
    
    From ec303bf1ca4ccecea4912ec782879fa123022438 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA" 
    Date: Fri, 29 Oct 2021 19:35:37 +0800
    Subject: [PATCH 400/490] Replace explicit calls to `Services::reset()` in
     tests
    
    ---
     tests/system/CodeIgniterTest.php                | 2 +-
     tests/system/CommonHelperTest.php               | 4 ++--
     tests/system/Config/ServicesTest.php            | 2 +-
     tests/system/Filters/FiltersTest.php            | 2 +-
     tests/system/HTTP/CURLRequestTest.php           | 2 +-
     tests/system/HTTP/URITest.php                   | 6 +++---
     tests/system/RESTful/ResourceControllerTest.php | 2 +-
     tests/system/RESTful/ResourcePresenterTest.php  | 2 +-
     8 files changed, 11 insertions(+), 11 deletions(-)
    
    diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php
    index 0e6de8eadda0..f7838105af4d 100644
    --- a/tests/system/CodeIgniterTest.php
    +++ b/tests/system/CodeIgniterTest.php
    @@ -36,7 +36,7 @@ final class CodeIgniterTest extends CIUnitTestCase
         protected function setUp(): void
         {
             parent::setUp();
    -        Services::reset();
    +        $this->resetServices();
     
             $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
     
    diff --git a/tests/system/CommonHelperTest.php b/tests/system/CommonHelperTest.php
    index 18e0a1022f8d..ce472e56b8be 100644
    --- a/tests/system/CommonHelperTest.php
    +++ b/tests/system/CommonHelperTest.php
    @@ -32,7 +32,7 @@ final class CommonHelperTest extends CIUnitTestCase
     
         protected function setUp(): void
         {
    -        Services::reset();
    +        $this->resetServices();
             parent::setUp();
             $this->cleanUpDummyHelpers();
         }
    @@ -41,7 +41,7 @@ protected function tearDown(): void
         {
             parent::tearDown();
             $this->cleanUpDummyHelpers();
    -        Services::reset();
    +        $this->resetServices();
         }
     
         private function createDummyHelpers(): void
    diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php
    index 2712bdce8dd5..77a0c77097fc 100644
    --- a/tests/system/Config/ServicesTest.php
    +++ b/tests/system/Config/ServicesTest.php
    @@ -64,7 +64,7 @@ protected function setUp(): void
         protected function tearDown(): void
         {
             $_SERVER = $this->original;
    -        Services::reset();
    +        $this->resetServices();
         }
     
         public function testCanReplaceFrameworkServices()
    diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
    index dae4074cd134..5451f4b239f4 100644
    --- a/tests/system/Filters/FiltersTest.php
    +++ b/tests/system/Filters/FiltersTest.php
    @@ -40,7 +40,7 @@ final class FiltersTest extends CIUnitTestCase
         protected function setUp(): void
         {
             parent::setUp();
    -        Services::reset();
    +        $this->resetServices();
     
             $defaults = [
                 'Config'        => APPPATH . 'Config',
    diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php
    index c1ac77765a44..694d8929dc09 100644
    --- a/tests/system/HTTP/CURLRequestTest.php
    +++ b/tests/system/HTTP/CURLRequestTest.php
    @@ -32,7 +32,7 @@ protected function setUp(): void
         {
             parent::setUp();
     
    -        Services::reset();
    +        $this->resetServices();
             $this->request = $this->getRequest();
         }
     
    diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php
    index 89cb106ed607..acd491bccb9d 100644
    --- a/tests/system/HTTP/URITest.php
    +++ b/tests/system/HTTP/URITest.php
    @@ -861,7 +861,7 @@ public function testSetBadSegmentSilent()
     
         public function testBasedNoIndex()
         {
    -        Services::reset();
    +        $this->resetServices();
     
             $_SERVER['HTTP_HOST']   = 'example.com';
             $_SERVER['REQUEST_URI'] = '/ci/v4/controller/method';
    @@ -888,7 +888,7 @@ public function testBasedNoIndex()
     
         public function testBasedWithIndex()
         {
    -        Services::reset();
    +        $this->resetServices();
     
             $_SERVER['HTTP_HOST']   = 'example.com';
             $_SERVER['REQUEST_URI'] = '/ci/v4/index.php/controller/method';
    @@ -915,7 +915,7 @@ public function testBasedWithIndex()
     
         public function testForceGlobalSecureRequests()
         {
    -        Services::reset();
    +        $this->resetServices();
     
             $_SERVER['HTTP_HOST']   = 'example.com';
             $_SERVER['REQUEST_URI'] = '/ci/v4/controller/method';
    diff --git a/tests/system/RESTful/ResourceControllerTest.php b/tests/system/RESTful/ResourceControllerTest.php
    index 185088da1505..ec8d13af7a2d 100644
    --- a/tests/system/RESTful/ResourceControllerTest.php
    +++ b/tests/system/RESTful/ResourceControllerTest.php
    @@ -54,7 +54,7 @@ protected function setUp(): void
         {
             parent::setUp();
     
    -        Services::reset();
    +        $this->resetServices();
     
             $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
     
    diff --git a/tests/system/RESTful/ResourcePresenterTest.php b/tests/system/RESTful/ResourcePresenterTest.php
    index e32ce9aba5d3..26ea1084b196 100644
    --- a/tests/system/RESTful/ResourcePresenterTest.php
    +++ b/tests/system/RESTful/ResourcePresenterTest.php
    @@ -48,7 +48,7 @@ protected function setUp(): void
         {
             parent::setUp();
     
    -        Services::reset();
    +        $this->resetServices();
     
             $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
     
    
    From 88c45d6947053ff76c44b8f028e5e27a1dc26845 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA"
     <51850998+paulbalandan@users.noreply.github.com>
    Date: Sat, 30 Oct 2021 00:16:12 +0800
    Subject: [PATCH 401/490] Fix wrong logic
    
    ---
     admin/starter/builds | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/admin/starter/builds b/admin/starter/builds
    index 4a3fe3d53139..0b10a150ac59 100755
    --- a/admin/starter/builds
    +++ b/admin/starter/builds
    @@ -112,7 +112,7 @@ foreach ($files as $file) {
         }
     }
     
    -if ($modified !== []) {
    +if ($modified === []) {
         echo 'No files modified.' . PHP_EOL;
     } else {
         echo 'The following files were modified:' . PHP_EOL;
    
    From a6c06cc990e73e3c1ee39004b5e51aabfbfa27f2 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Sat, 30 Oct 2021 08:58:11 +0900
    Subject: [PATCH 402/490] refactor: remove unnecessary second param
    
    ---
     tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    index ae6aef2ad74d..fd440bbcffdd 100644
    --- a/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    +++ b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    @@ -225,7 +225,7 @@ public function testHeaderContentLengthNotSharedBetweenRequests()
             $options = [
                 'base_uri' => 'http://www.foo.com/api/v1/',
             ];
    -        $request = $this->getRequest($options, false);
    +        $request = $this->getRequest($options);
     
             $request->post('example', [
                 'form_params' => [
    
    From f53a1da13bec07c5a30a2f658a6658e14d1dbaa9 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Sat, 30 Oct 2021 09:22:54 +0900
    Subject: [PATCH 403/490] refactor: move Config\App::$CURLRequestShareOptions
     to Config\CURLRequest::$shareOptions
    
    ---
     app/Config/App.php                            | 11 -----------
     app/Config/CURLRequest.php                    | 19 +++++++++++++++++++
     system/HTTP/CURLRequest.php                   |  8 ++++++--
     .../HTTP/CURLRequestDoNotShareOptionsTest.php | 11 ++++++++---
     tests/system/HTTP/CURLRequestTest.php         | 11 ++++++++---
     5 files changed, 41 insertions(+), 19 deletions(-)
     create mode 100644 app/Config/CURLRequest.php
    
    diff --git a/app/Config/App.php b/app/Config/App.php
    index 57c9b8a45c46..88b295e9a010 100644
    --- a/app/Config/App.php
    +++ b/app/Config/App.php
    @@ -461,15 +461,4 @@ class App extends BaseConfig
          * @var bool
          */
         public $CSPEnabled = false;
    -
    -    /**
    -     * --------------------------------------------------------------------------
    -     * CURLRequest Share Options
    -     * --------------------------------------------------------------------------
    -     *
    -     * Whether share options between requests or not.
    -     *
    -     * @var bool
    -     */
    -    public $CURLRequestShareOptions = true;
     }
    diff --git a/app/Config/CURLRequest.php b/app/Config/CURLRequest.php
    new file mode 100644
    index 000000000000..c16cffac4e0a
    --- /dev/null
    +++ b/app/Config/CURLRequest.php
    @@ -0,0 +1,19 @@
    +response       = $response;
             $this->baseURI        = $uri->useRawQueryString();
             $this->defaultOptions = $options;
    -        $this->config         = $this->defaultConfig;
    -        $this->shareOptions   = $config->CURLRequestShareOptions ?? true;
     
    +        /** @var ConfigCURLRequest|null $configCURLRequest */
    +        $configCURLRequest  = config('CURLRequest');
    +        $this->shareOptions = $configCURLRequest->shareOptions ?? true;
    +
    +        $this->config = $this->defaultConfig;
             $this->parseOptions($options);
         }
     
    diff --git a/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    index fd440bbcffdd..4d35d4510271 100644
    --- a/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    +++ b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php
    @@ -11,11 +11,13 @@
     
     namespace CodeIgniter\HTTP;
     
    +use CodeIgniter\Config\Factories;
     use CodeIgniter\Config\Services;
     use CodeIgniter\HTTP\Exceptions\HTTPException;
     use CodeIgniter\Test\CIUnitTestCase;
     use CodeIgniter\Test\Mock\MockCURLRequest;
     use Config\App;
    +use Config\CURLRequest as ConfigCURLRequest;
     use CURLFile;
     
     /**
    @@ -38,9 +40,12 @@ protected function setUp(): void
     
         protected function getRequest(array $options = [])
         {
    -        $uri                          = isset($options['base_uri']) ? new URI($options['base_uri']) : new URI();
    -        $app                          = new App();
    -        $app->CURLRequestShareOptions = false;
    +        $uri = isset($options['base_uri']) ? new URI($options['base_uri']) : new URI();
    +        $app = new App();
    +
    +        $config               = new ConfigCURLRequest();
    +        $config->shareOptions = false;
    +        Factories::injectMock('config', 'CURLRequest', $config);
     
             return new MockCURLRequest(($app), $uri, new Response($app), $options);
         }
    diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php
    index de7ef08b59fc..64562f37fd65 100644
    --- a/tests/system/HTTP/CURLRequestTest.php
    +++ b/tests/system/HTTP/CURLRequestTest.php
    @@ -11,11 +11,13 @@
     
     namespace CodeIgniter\HTTP;
     
    +use CodeIgniter\Config\Factories;
     use CodeIgniter\Config\Services;
     use CodeIgniter\HTTP\Exceptions\HTTPException;
     use CodeIgniter\Test\CIUnitTestCase;
     use CodeIgniter\Test\Mock\MockCURLRequest;
     use Config\App;
    +use Config\CURLRequest as ConfigCURLRequest;
     use CURLFile;
     
     /**
    @@ -38,9 +40,12 @@ protected function setUp(): void
     
         protected function getRequest(array $options = [])
         {
    -        $uri                          = isset($options['base_uri']) ? new URI($options['base_uri']) : new URI();
    -        $app                          = new App();
    -        $app->CURLRequestShareOptions = true;
    +        $uri = isset($options['base_uri']) ? new URI($options['base_uri']) : new URI();
    +        $app = new App();
    +
    +        $config               = new ConfigCURLRequest();
    +        $config->shareOptions = true;
    +        Factories::injectMock('config', 'CURLRequest', $config);
     
             return new MockCURLRequest(($app), $uri, new Response($app), $options);
         }
    
    From 772a40819e99c8f5d97ff97412d8ae0f96ae562e Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Sat, 30 Oct 2021 09:35:49 +0900
    Subject: [PATCH 404/490] fix: change from private to protected
    
    ---
     system/HTTP/CURLRequest.php | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php
    index 1b8b474e5022..5f857d9319b5 100644
    --- a/system/HTTP/CURLRequest.php
    +++ b/system/HTTP/CURLRequest.php
    @@ -50,7 +50,7 @@ class CURLRequest extends Request
          *
          * @var array
          */
    -    private $defaultConfig = [
    +    protected $defaultConfig = [
             'timeout'         => 0.0,
             'connect_timeout' => 150,
             'debug'           => false,
    @@ -149,7 +149,7 @@ public function request($method, string $url, array $options = []): ResponseInte
             return $this->response;
         }
     
    -    private function resetOptions()
    +    protected function resetOptions()
         {
             // Reset headers
             $this->headers   = [];
    
    From 3901496bc283871a0e02da72923d0dc0eb274181 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Sat, 30 Oct 2021 09:39:58 +0900
    Subject: [PATCH 405/490] docs: add PHPDoc comments
    
    ---
     app/Config/CURLRequest.php  | 3 +++
     system/HTTP/CURLRequest.php | 8 +++++++-
     2 files changed, 10 insertions(+), 1 deletion(-)
    
    diff --git a/app/Config/CURLRequest.php b/app/Config/CURLRequest.php
    index c16cffac4e0a..b4c8e5c4f13c 100644
    --- a/app/Config/CURLRequest.php
    +++ b/app/Config/CURLRequest.php
    @@ -13,6 +13,9 @@ class CURLRequest extends BaseConfig
          *
          * Whether share options between requests or not.
          *
    +     * If true, all the options won't be reset between requests.
    +     * It may cause an error request with unnecessary headers.
    +     *
          * @var bool
          */
         public $shareOptions = true;
    diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php
    index 5f857d9319b5..a3c92511adda 100644
    --- a/system/HTTP/CURLRequest.php
    +++ b/system/HTTP/CURLRequest.php
    @@ -81,7 +81,7 @@ class CURLRequest extends Request
         protected $delay = 0.0;
     
         /**
    -     * Default options. Applied to all requests.
    +     * The default options from the constructor. Applied to all requests.
          *
          * @var array
          */
    @@ -90,6 +90,9 @@ class CURLRequest extends Request
         /**
          * Whether share options between requests or not.
          *
    +     * If true, all the options won't be reset between requests.
    +     * It may cause an error request with unnecessary headers.
    +     *
          * @var bool
          */
         private $shareOptions;
    @@ -149,6 +152,9 @@ public function request($method, string $url, array $options = []): ResponseInte
             return $this->response;
         }
     
    +    /**
    +     * Reset all options to default.
    +     */
         protected function resetOptions()
         {
             // Reset headers
    
    From 8a5973b0de4f4cdf6d2c89006d6a4bf91847e8de Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Sat, 30 Oct 2021 09:55:09 +0900
    Subject: [PATCH 406/490] config: add curlrequest.shareOptions
    
    ---
     env | 6 ++++++
     1 file changed, 6 insertions(+)
    
    diff --git a/env b/env
    index 0b6aa1720782..6e30e34b7ddb 100644
    --- a/env
    +++ b/env
    @@ -124,3 +124,9 @@
     #--------------------------------------------------------------------
     
     # logger.threshold = 4
    +
    +#--------------------------------------------------------------------
    +# CURLRequest
    +#--------------------------------------------------------------------
    +
    +# curlrequest.shareOptions = true
    
    From 88963ba2e85f9e0c731ed56c8fdb32108289e757 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Sat, 30 Oct 2021 10:31:47 +0900
    Subject: [PATCH 407/490] docs: add about CURLRequest $shareOptions
    
    ---
     .../source/libraries/curlrequest.rst          | 21 ++++++++++++++++++-
     1 file changed, 20 insertions(+), 1 deletion(-)
    
    diff --git a/user_guide_src/source/libraries/curlrequest.rst b/user_guide_src/source/libraries/curlrequest.rst
    index c9c69ea7ae08..0074523d79fd 100644
    --- a/user_guide_src/source/libraries/curlrequest.rst
    +++ b/user_guide_src/source/libraries/curlrequest.rst
    @@ -19,6 +19,21 @@ to change very little to move over to use Guzzle.
         in your version of PHP. This is a very common library that is typically available but not all hosts
         will provide it, so please check with your host to verify if you run into problems.
     
    +**********************
    +Config for CURLRequest
    +**********************
    +
    +Sharing Options
    +===============
    +
    +Due to historical reasons, by default, the CURLRequest shares all the options between requests.
    +If you send more than one request with an instance of the class,
    +this behavior may cause an error request with unnecessary headers.
    +
    +You can change the behavior by editing the following config parameter value in **app/Config/CURLRequest.php** to ``false``::
    +
    +    public $shareOptions = false;
    +
     *******************
     Loading the Library
     *******************
    @@ -38,9 +53,11 @@ The options are described later in this document::
         ];
         $client = \Config\Services::curlrequest($options);
     
    +.. note:: When ``$shareOptions`` is false, the default options passed to the class constructor will be used for all requests. Other options will be reset after sending a request.
    +
     When creating the class manually, you need to pass a few dependencies in. The first parameter is an
     instance of the ``Config\App`` class. The second parameter is a URI instance. The third
    -parameter is a Response object. The fourth parameter is the optional ``$options`` array::
    +parameter is a Response object. The fourth parameter is the optional default ``$options`` array::
     
         $client = new \CodeIgniter\HTTP\CURLRequest(
             new \Config\App(),
    @@ -70,6 +87,8 @@ a Response instance to you. This takes the HTTP method, the url and an array of
             'auth' => ['user', 'pass'],
         ]);
     
    +.. note:: When ``$shareOptions`` is false, the options passed to the method will be used for the request. After sending the request, they will be cleared. If you want to use the options to all requests, pass the options in the constructor.
    +
     Since the response is an instance of ``CodeIgniter\HTTP\Response`` you have all of the normal information
     available to you::
     
    
    From 916a7b01325edd3ad7e1e30068654337a1103205 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Sun, 31 Oct 2021 11:37:54 +0900
    Subject: [PATCH 408/490] refactor: remove static variable and add a proptery
     for it
    
    It makes escape chars wrong on PHP 8.1.
    See
    Usage of static Variables in Inherited Methods
    https://www.php.net/manual/en/migration81.incompatible.php
    ---
     system/Database/BaseConnection.php | 21 +++++++++++++--------
     1 file changed, 13 insertions(+), 8 deletions(-)
    
    diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php
    index 9cb44e246296..8525755f6950 100644
    --- a/system/Database/BaseConnection.php
    +++ b/system/Database/BaseConnection.php
    @@ -238,6 +238,13 @@ abstract class BaseConnection implements ConnectionInterface
          */
         public $likeEscapeChar = '!';
     
    +    /**
    +     * RegExp used to escape identifiers
    +     *
    +     * @var array
    +     */
    +    protected $pregEscapeChar = [];
    +
         /**
          * Holds previously looked up data
          * for performance reasons.
    @@ -1119,29 +1126,27 @@ public function escapeIdentifiers($item)
                 return $item;
             }
     
    -        static $pregEc = [];
    -
    -        if (empty($pregEc)) {
    +        if (empty($this->pregEscapeChar)) {
                 if (is_array($this->escapeChar)) {
    -                $pregEc = [
    +                $this->pregEscapeChar = [
                         preg_quote($this->escapeChar[0], '/'),
                         preg_quote($this->escapeChar[1], '/'),
                         $this->escapeChar[0],
                         $this->escapeChar[1],
                     ];
                 } else {
    -                $pregEc[0] = $pregEc[1] = preg_quote($this->escapeChar, '/');
    -                $pregEc[2] = $pregEc[3] = $this->escapeChar;
    +                $this->pregEscapeChar[0] = $this->pregEscapeChar[1] = preg_quote($this->escapeChar, '/');
    +                $this->pregEscapeChar[2] = $this->pregEscapeChar[3] = $this->escapeChar;
                 }
             }
     
             foreach ($this->reservedIdentifiers as $id) {
                 if (strpos($item, '.' . $id) !== false) {
    -                return preg_replace('/' . $pregEc[0] . '?([^' . $pregEc[1] . '\.]+)' . $pregEc[1] . '?\./i', $pregEc[2] . '$1' . $pregEc[3] . '.', $item);
    +                return preg_replace('/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i', $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.', $item);
                 }
             }
     
    -        return preg_replace('/' . $pregEc[0] . '?([^' . $pregEc[1] . '\.]+)' . $pregEc[1] . '?(\.)?/i', $pregEc[2] . '$1' . $pregEc[3] . '$2', $item);
    +        return preg_replace('/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i', $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2', $item);
         }
     
         /**
    
    From 601d29523f7a921655b471931c02e9c505b191e7 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Sun, 31 Oct 2021 11:44:41 +0900
    Subject: [PATCH 409/490] refactor: break long lines
    
    ---
     system/Database/BaseConnection.php | 12 ++++++++++--
     1 file changed, 10 insertions(+), 2 deletions(-)
    
    diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php
    index 8525755f6950..3a50e43ffd21 100644
    --- a/system/Database/BaseConnection.php
    +++ b/system/Database/BaseConnection.php
    @@ -1142,11 +1142,19 @@ public function escapeIdentifiers($item)
     
             foreach ($this->reservedIdentifiers as $id) {
                 if (strpos($item, '.' . $id) !== false) {
    -                return preg_replace('/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i', $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.', $item);
    +                return preg_replace(
    +                    '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i',
    +                    $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.',
    +                    $item
    +                );
                 }
             }
     
    -        return preg_replace('/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i', $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2', $item);
    +        return preg_replace(
    +            '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i',
    +            $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2',
    +            $item
    +        );
         }
     
         /**
    
    From c36ce02b4b5c9686d632312b75237434e237fe6e Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Sun, 31 Oct 2021 11:57:51 +0900
    Subject: [PATCH 410/490] refactor: remove static variables and add properties
    
    To prevent inappropriate behavior when inheriting by using the values of the parent class on PHP 8.1.
    
    Usage of static Variables in Inherited Methods
    https://www.php.net/manual/en/migration81.incompatible.php
    ---
     system/Database/BaseBuilder.php | 30 ++++++++++++++++++++----------
     1 file changed, 20 insertions(+), 10 deletions(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index f87f21bf7308..73865aed79ee 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -244,6 +244,20 @@ class BaseBuilder
             'RIGHT OUTER',
         ];
     
    +    /**
    +     * Strings that determine if a string represents a literal value or a field name
    +     *
    +     * @var string[]
    +     */
    +    protected $isLiteralStr = [];
    +
    +    /**
    +     * RegExp used to get operators
    +     *
    +     * @var string[]
    +     */
    +    protected $pregOperators = [];
    +
         /**
          * Constructor
          *
    @@ -2515,13 +2529,11 @@ protected function isLiteral(string $str): bool
                 return true;
             }
     
    -        static $_str;
    -
    -        if (empty($_str)) {
    -            $_str = ($this->db->escapeChar !== '"') ? ['"', "'"] : ["'"];
    +        if (empty($this->isLiteralStr)) {
    +            $this->isLiteralStr = ($this->db->escapeChar !== '"') ? ['"', "'"] : ["'"];
             }
     
    -        return in_array($str[0], $_str, true);
    +        return in_array($str[0], $this->isLiteralStr, true);
         }
     
         /**
    @@ -2610,11 +2622,9 @@ protected function hasOperator(string $str): bool
          */
         protected function getOperator(string $str, bool $list = false)
         {
    -        static $_operators;
    -
    -        if (empty($_operators)) {
    +        if (empty($this->pregOperators)) {
                 $_les       = ($this->db->likeEscapeStr !== '') ? '\s+' . preg_quote(trim(sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar)), '/') : '';
    -            $_operators = [
    +            $this->pregOperators = [
                     '\s*(?:<|>|!)?=\s*', // =, <=, >=, !=
                     '\s*<>?\s*', // <, <>
                     '\s*>\s*', // >
    @@ -2630,7 +2640,7 @@ protected function getOperator(string $str, bool $list = false)
                 ];
             }
     
    -        return preg_match_all('/' . implode('|', $_operators) . '/i', $str, $match) ? ($list ? $match[0] : $match[0][0]) : false;
    +        return preg_match_all('/' . implode('|', $this->pregOperators) . '/i', $str, $match) ? ($list ? $match[0] : $match[0][0]) : false;
         }
     
         /**
    
    From dbfacc0fefca07f68dbbc953dccee066124092a4 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Sun, 31 Oct 2021 12:03:04 +0900
    Subject: [PATCH 411/490] refactor: break long lines
    
    ---
     system/Database/BaseBuilder.php | 15 ++++++++++++---
     1 file changed, 12 insertions(+), 3 deletions(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index 73865aed79ee..150ed3b5cf5a 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -2612,7 +2612,10 @@ protected function resetWrite()
          */
         protected function hasOperator(string $str): bool
         {
    -        return (bool) preg_match('/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i', trim($str));
    +        return (bool) preg_match(
    +            '/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i',
    +            trim($str)
    +        );
         }
     
         /**
    @@ -2623,7 +2626,9 @@ protected function hasOperator(string $str): bool
         protected function getOperator(string $str, bool $list = false)
         {
             if (empty($this->pregOperators)) {
    -            $_les       = ($this->db->likeEscapeStr !== '') ? '\s+' . preg_quote(trim(sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar)), '/') : '';
    +            $_les = ($this->db->likeEscapeStr !== '')
    +                ? '\s+' . preg_quote(trim(sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar)), '/')
    +                : '';
                 $this->pregOperators = [
                     '\s*(?:<|>|!)?=\s*', // =, <=, >=, !=
                     '\s*<>?\s*', // <, <>
    @@ -2640,7 +2645,11 @@ protected function getOperator(string $str, bool $list = false)
                 ];
             }
     
    -        return preg_match_all('/' . implode('|', $this->pregOperators) . '/i', $str, $match) ? ($list ? $match[0] : $match[0][0]) : false;
    +        return preg_match_all(
    +            '/' . implode('|', $this->pregOperators) . '/i',
    +            $str,
    +            $match
    +        ) ? ($list ? $match[0] : $match[0][0]) : false;
         }
     
         /**
    
    From 833ca561ac26798d626a7d732f02b90012c6457c Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA"
     <51850998+paulbalandan@users.noreply.github.com>
    Date: Sun, 31 Oct 2021 18:39:55 +0800
    Subject: [PATCH 412/490] Apply suggestions from code review
    
    ---
     system/Database/BaseBuilder.php | 12 ++++++------
     1 file changed, 6 insertions(+), 6 deletions(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index 150ed3b5cf5a..886ce928e132 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -2529,8 +2529,8 @@ protected function isLiteral(string $str): bool
                 return true;
             }
     
    -        if (empty($this->isLiteralStr)) {
    -            $this->isLiteralStr = ($this->db->escapeChar !== '"') ? ['"', "'"] : ["'"];
    +        if ($this->isLiteralStr === []) {
    +            $this->isLiteralStr = $this->db->escapeChar !== '"' ? ['"', "'"] : ["'"];
             }
     
             return in_array($str[0], $this->isLiteralStr, true);
    @@ -2612,10 +2612,10 @@ protected function resetWrite()
          */
         protected function hasOperator(string $str): bool
         {
    -        return (bool) preg_match(
    +        return preg_match(
                 '/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i',
                 trim($str)
    -        );
    +        ) === 1;
         }
     
         /**
    @@ -2625,8 +2625,8 @@ protected function hasOperator(string $str): bool
          */
         protected function getOperator(string $str, bool $list = false)
         {
    -        if (empty($this->pregOperators)) {
    -            $_les = ($this->db->likeEscapeStr !== '')
    +        if ($this->pregOperators === []) {
    +            $_les = $this->db->likeEscapeStr !== ''
                     ? '\s+' . preg_quote(trim(sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar)), '/')
                     : '';
                 $this->pregOperators = [
    
    From 8d5dc08576f433b432d383e9192c51b077366cd3 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA"
     <51850998+paulbalandan@users.noreply.github.com>
    Date: Sun, 31 Oct 2021 18:41:59 +0800
    Subject: [PATCH 413/490] Change empty to is identical check
    
    ---
     system/Database/BaseConnection.php | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php
    index 3a50e43ffd21..ebb63b7f5827 100644
    --- a/system/Database/BaseConnection.php
    +++ b/system/Database/BaseConnection.php
    @@ -1126,7 +1126,7 @@ public function escapeIdentifiers($item)
                 return $item;
             }
     
    -        if (empty($this->pregEscapeChar)) {
    +        if ($this->pregEscapeChar === []) {
                 if (is_array($this->escapeChar)) {
                     $this->pregEscapeChar = [
                         preg_quote($this->escapeChar[0], '/'),
    
    From ef7d23dbd340649302abd90d50c246f9c54cedfa Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA" 
    Date: Sun, 31 Oct 2021 19:37:52 +0800
    Subject: [PATCH 414/490] Replace usage of `FILTER_SANITIZE_STRING`
    
    ---
     system/HTTP/CLIRequest.php        |  8 +++-----
     system/HTTP/CURLRequest.php       | 12 +++---------
     system/Router/RouteCollection.php |  8 ++++----
     3 files changed, 10 insertions(+), 18 deletions(-)
    
    diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php
    index b8db857fce54..fcc2a51389d2 100644
    --- a/system/HTTP/CLIRequest.php
    +++ b/system/HTTP/CLIRequest.php
    @@ -15,8 +15,6 @@
     use RuntimeException;
     
     /**
    - * Class CLIRequest
    - *
      * Represents a request from the command-line. Provides additional
      * tools to interact with that request since CLI requests are not
      * static like HTTP requests might be.
    @@ -172,17 +170,17 @@ protected function parseCommand()
                     if ($optionValue) {
                         $optionValue = false;
                     } else {
    -                    $this->segments[] = filter_var($arg, FILTER_SANITIZE_STRING);
    +                    $this->segments[] = esc(strip_tags($arg));
                     }
     
                     continue;
                 }
     
    -            $arg   = filter_var(ltrim($arg, '-'), FILTER_SANITIZE_STRING);
    +            $arg   = esc(strip_tags(ltrim($arg, '-')));
                 $value = null;
     
                 if (isset($args[$i + 1]) && mb_strpos($args[$i + 1], '-') !== 0) {
    -                $value       = filter_var($args[$i + 1], FILTER_SANITIZE_STRING);
    +                $value       = esc(strip_tags($args[$i + 1]));
                     $optionValue = true;
                 }
     
    diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php
    index 3be8214d42b4..cb723c31adff 100644
    --- a/system/HTTP/CURLRequest.php
    +++ b/system/HTTP/CURLRequest.php
    @@ -16,10 +16,7 @@
     use InvalidArgumentException;
     
     /**
    - * Class OutgoingRequest
    - *
    - * A lightweight HTTP client for sending synchronous HTTP requests
    - * via cURL.
    + * A lightweight HTTP client for sending synchronous HTTP requests via cURL.
      */
     class CURLRequest extends Request
     {
    @@ -84,10 +81,7 @@ class CURLRequest extends Request
         public function __construct(App $config, URI $uri, ?ResponseInterface $response = null, array $options = [])
         {
             if (! function_exists('curl_version')) {
    -            // we won't see this during travis-CI
    -            // @codeCoverageIgnoreStart
    -            throw HTTPException::forMissingCurl();
    -            // @codeCoverageIgnoreEnd
    +            throw HTTPException::forMissingCurl(); // @codeCoverageIgnore
             }
     
             parent::__construct($config);
    @@ -110,7 +104,7 @@ public function request($method, string $url, array $options = []): ResponseInte
     
             $url = $this->prepareURL($url);
     
    -        $method = filter_var($method, FILTER_SANITIZE_STRING);
    +        $method = esc(strip_tags($method));
     
             $this->send($method, $url);
     
    diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php
    index 1dcbfe9f590d..15a2d3d79e3e 100644
    --- a/system/Router/RouteCollection.php
    +++ b/system/Router/RouteCollection.php
    @@ -19,8 +19,6 @@
     use InvalidArgumentException;
     
     /**
    - * Class RouteCollection
    - *
      * @todo Implement nested resource routing (See CakePHP)
      */
     class RouteCollection implements RouteCollectionInterface
    @@ -663,10 +661,11 @@ public function resource(string $name, ?array $options = null): RouteCollectionI
             // resources are sent to, we need to have a new name
             // to store the values in.
             $newName = implode('\\', array_map('ucfirst', explode('/', $name)));
    +
             // If a new controller is specified, then we replace the
             // $name value with the name of the new controller.
             if (isset($options['controller'])) {
    -            $newName = ucfirst(filter_var($options['controller'], FILTER_SANITIZE_STRING));
    +            $newName = ucfirst(esc(strip_tags($options['controller'])));
             }
     
             // In order to allow customization of allowed id values
    @@ -756,10 +755,11 @@ public function presenter(string $name, ?array $options = null): RouteCollection
             // resources are sent to, we need to have a new name
             // to store the values in.
             $newName = implode('\\', array_map('ucfirst', explode('/', $name)));
    +
             // If a new controller is specified, then we replace the
             // $name value with the name of the new controller.
             if (isset($options['controller'])) {
    -            $newName = ucfirst(filter_var($options['controller'], FILTER_SANITIZE_STRING));
    +            $newName = ucfirst(esc(strip_tags($options['controller'])));
             }
     
             // In order to allow customization of allowed id values
    
    From 6a689bf9768bcb660bfba3886b640ab5d5b3c4b3 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA" 
    Date: Sun, 31 Oct 2021 20:36:38 +0800
    Subject: [PATCH 415/490] Enable `ordered_class_elements` rule
    
    ---
     .no-header.php-cs-fixer.dist.php                 | 12 +++++++++++-
     .php-cs-fixer.dist.php                           | 12 +++++++++++-
     tests/_support/Publishers/TestPublisher.php      | 16 ++++++++--------
     tests/system/Cache/Handlers/FileHandlerTest.php  |  3 +--
     .../Cache/Handlers/MemcachedHandlerTest.php      |  4 ++--
     .../system/Cache/Handlers/PredisHandlerTest.php  |  4 ++--
     tests/system/Cache/Handlers/RedisHandlerTest.php |  4 ++--
     7 files changed, 37 insertions(+), 18 deletions(-)
    
    diff --git a/.no-header.php-cs-fixer.dist.php b/.no-header.php-cs-fixer.dist.php
    index 153ac5bd1328..9417a859e07e 100644
    --- a/.no-header.php-cs-fixer.dist.php
    +++ b/.no-header.php-cs-fixer.dist.php
    @@ -30,7 +30,17 @@
             __DIR__ . '/admin/starter/builds',
         ]);
     
    -$overrides = [];
    +$overrides = [
    +    'ordered_class_elements' => [
    +        'order' => [
    +            'use_trait',
    +            'constant',
    +            'property',
    +            'method',
    +        ],
    +        'sort_algorithm' => 'none',
    +    ],
    +];
     
     $options = [
         'cacheFile'    => 'build/.no-header.php-cs-fixer.cache',
    diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
    index 3c9e05fe3b74..5df3c5e90a52 100644
    --- a/.php-cs-fixer.dist.php
    +++ b/.php-cs-fixer.dist.php
    @@ -34,7 +34,17 @@
             __DIR__ . '/spark',
         ]);
     
    -$overrides = [];
    +$overrides = [
    +    'ordered_class_elements' => [
    +        'order' => [
    +            'use_trait',
    +            'constant',
    +            'property',
    +            'method',
    +        ],
    +        'sort_algorithm' => 'none',
    +    ],
    +];
     
     $options = [
         'cacheFile'    => 'build/.php-cs-fixer.cache',
    diff --git a/tests/_support/Publishers/TestPublisher.php b/tests/_support/Publishers/TestPublisher.php
    index 357e50f17e1a..95d9fa72c250 100644
    --- a/tests/_support/Publishers/TestPublisher.php
    +++ b/tests/_support/Publishers/TestPublisher.php
    @@ -15,14 +15,6 @@
     
     final class TestPublisher extends Publisher
     {
    -    /**
    -     * Fakes an error on the given file.
    -     */
    -    public static function setResult(bool $result)
    -    {
    -        self::$result = $result;
    -    }
    -
         /**
          * Return value for publish()
          *
    @@ -44,6 +36,14 @@ public static function setResult(bool $result)
          */
         protected $destination = WRITEPATH;
     
    +    /**
    +     * Fakes an error on the given file.
    +     */
    +    public static function setResult(bool $result)
    +    {
    +        self::$result = $result;
    +    }
    +
         /**
          * Fakes a publish event so no files are actually copied.
          */
    diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php
    index 3f82375354fe..f0aa76ea610d 100644
    --- a/tests/system/Cache/Handlers/FileHandlerTest.php
    +++ b/tests/system/Cache/Handlers/FileHandlerTest.php
    @@ -20,6 +20,7 @@
     final class FileHandlerTest extends AbstractHandlerTest
     {
         private static $directory = 'FileHandler';
    +    private $config;
     
         private static function getKeyArray()
         {
    @@ -30,8 +31,6 @@ private static function getKeyArray()
             ];
         }
     
    -    private $config;
    -
         protected function setUp(): void
         {
             parent::setUp();
    diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php
    index 240225fd8bc0..38005d87ed5b 100644
    --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php
    +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php
    @@ -22,6 +22,8 @@
      */
     final class MemcachedHandlerTest extends AbstractHandlerTest
     {
    +    private $config;
    +
         private static function getKeyArray()
         {
             return [
    @@ -31,8 +33,6 @@ private static function getKeyArray()
             ];
         }
     
    -    private $config;
    -
         protected function setUp(): void
         {
             parent::setUp();
    diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php
    index 9b762f9c27f1..cdc79adf68b4 100644
    --- a/tests/system/Cache/Handlers/PredisHandlerTest.php
    +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php
    @@ -21,6 +21,8 @@
      */
     final class PredisHandlerTest extends AbstractHandlerTest
     {
    +    private $config;
    +
         private static function getKeyArray()
         {
             return [
    @@ -30,8 +32,6 @@ private static function getKeyArray()
             ];
         }
     
    -    private $config;
    -
         protected function setUp(): void
         {
             parent::setUp();
    diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php
    index 32d0cdf8223e..4eb8f8b08560 100644
    --- a/tests/system/Cache/Handlers/RedisHandlerTest.php
    +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php
    @@ -21,6 +21,8 @@
      */
     final class RedisHandlerTest extends AbstractHandlerTest
     {
    +    private $config;
    +
         private static function getKeyArray()
         {
             return [
    @@ -30,8 +32,6 @@ private static function getKeyArray()
             ];
         }
     
    -    private $config;
    -
         protected function setUp(): void
         {
             parent::setUp();
    
    From d8b750b3b815fc935e5f0e48bd1903d6f12a3c46 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 26 Oct 2021 15:41:11 +0900
    Subject: [PATCH 416/490] test: refactor: $request -> $this->request
    
    ---
     tests/system/Filters/FiltersTest.php | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
    index 5451f4b239f4..302aa985e474 100644
    --- a/tests/system/Filters/FiltersTest.php
    +++ b/tests/system/Filters/FiltersTest.php
    @@ -61,8 +61,8 @@ public function testProcessMethodDetectsCLI()
                     'cli' => ['foo'],
                 ],
             ];
    -        $request = new CLIRequest(new MockAppConfig());
    -        $filters = new Filters((object) $config, $request, $this->response);
    +        $this->request = new CLIRequest(new MockAppConfig());
    +        $filters       = new Filters((object) $config, $this->request, $this->response);
     
             $expected = [
                 'before' => ['foo'],
    
    From 1f4ba303f9ef953fcd932a78f4bc224819fc241d Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 26 Oct 2021 15:49:57 +0900
    Subject: [PATCH 417/490] test: refactor: reset $_SERVER
    
    ---
     tests/system/Filters/FiltersTest.php | 8 ++++++++
     1 file changed, 8 insertions(+)
    
    diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
    index 302aa985e474..ce01c7757004 100644
    --- a/tests/system/Filters/FiltersTest.php
    +++ b/tests/system/Filters/FiltersTest.php
    @@ -50,11 +50,19 @@ protected function setUp(): void
     
             Services::autoloader()->addNamespace($defaults);
     
    +        $_SERVER = [];
    +
             $this->response = Services::response();
         }
     
         public function testProcessMethodDetectsCLI()
         {
    +        $_SERVER['argv'] = [
    +            'spark',
    +            'list',
    +        ];
    +        $_SERVER['argc'] = 2;
    +
             $config = [
                 'aliases' => ['foo' => ''],
                 'methods' => [
    
    From 3ea1ede69a0c78f33251ef6ba0fde57d19274188 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 26 Oct 2021 15:50:44 +0900
    Subject: [PATCH 418/490] test: refactor: change the position of line breaks
    
    Separate arrange, act, assert.
    ---
     tests/system/Filters/FiltersTest.php | 92 +++++++++++-----------------
     1 file changed, 36 insertions(+), 56 deletions(-)
    
    diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
    index ce01c7757004..57c3f06753f3 100644
    --- a/tests/system/Filters/FiltersTest.php
    +++ b/tests/system/Filters/FiltersTest.php
    @@ -40,6 +40,7 @@ final class FiltersTest extends CIUnitTestCase
         protected function setUp(): void
         {
             parent::setUp();
    +
             $this->resetServices();
     
             $defaults = [
    @@ -47,7 +48,6 @@ protected function setUp(): void
                 'App'           => APPPATH,
                 'Tests\Support' => TESTPATH . '_support',
             ];
    -
             Services::autoloader()->addNamespace($defaults);
     
             $_SERVER = [];
    @@ -76,7 +76,6 @@ public function testProcessMethodDetectsCLI()
                 'before' => ['foo'],
                 'after'  => [],
             ];
    -
             $this->assertSame($expected, $filters->initialize()->getFilters());
         }
     
    @@ -97,7 +96,6 @@ public function testProcessMethodDetectsGetRequests()
                 'before' => ['foo'],
                 'after'  => [],
             ];
    -
             $this->assertSame($expected, $filters->initialize()->getFilters());
         }
     
    @@ -122,7 +120,6 @@ public function testProcessMethodRespectsMethod()
                 'before' => ['bar'],
                 'after'  => [],
             ];
    -
             $this->assertSame($expected, $filters->initialize()->getFilters());
         }
     
    @@ -147,7 +144,6 @@ public function testProcessMethodIgnoresMethod()
                 'before' => [],
                 'after'  => [],
             ];
    -
             $this->assertSame($expected, $filters->initialize()->getFilters());
         }
     
    @@ -181,7 +177,6 @@ public function testProcessMethodProcessGlobals()
                 ],
                 'after' => ['baz'],
             ];
    -
             $this->assertSame($expected, $filters->initialize()->getFilters());
         }
     
    @@ -222,15 +217,14 @@ public function testProcessMethodProcessGlobalsWithExcept(array $except)
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $expected = [
                 'before' => [
                     'bar',
                 ],
                 'after' => ['baz'],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -253,13 +247,12 @@ public function testProcessMethodProcessesFiltersBefore()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $expected = [
                 'before' => ['foo'],
                 'after'  => [],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -282,15 +275,14 @@ public function testProcessMethodProcessesFiltersAfter()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'users/foo/bar';
     
    +        $uri      = 'users/foo/bar';
             $expected = [
                 'before' => [],
                 'after'  => [
                     'foo',
                 ],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -329,8 +321,8 @@ public function testProcessMethodProcessesCombined()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $expected = [
                 'before' => [
                     'barg',
    @@ -339,7 +331,6 @@ public function testProcessMethodProcessesCombined()
                 ],
                 'after' => ['bazg'],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -371,8 +362,8 @@ public function testProcessMethodProcessesCombinedAfterForToolbar()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $expected = [
                 'before' => ['bar'],
                 'after'  => [
    @@ -381,7 +372,6 @@ public function testProcessMethodProcessesCombinedAfterForToolbar()
                     'toolbar',
                 ],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -400,8 +390,8 @@ public function testRunThrowsWithInvalidAlias()
             $filters       = new Filters((object) $config, $this->request, $this->response);
     
             $this->expectException(FilterException::class);
    -        $uri = 'admin/foo/bar';
     
    +        $uri = 'admin/foo/bar';
             $filters->run($uri);
         }
     
    @@ -418,8 +408,8 @@ public function testCustomFiltersLoad()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri     = 'admin/foo/bar';
             $request = $filters->run($uri, 'before');
     
             $this->assertSame('http://hellowworld.com', $request->url);
    @@ -440,8 +430,8 @@ public function testRunThrowsWithInvalidClassType()
             $filters       = new Filters((object) $config, $this->request, $this->response);
     
             $this->expectException(FilterException::class);
    -        $uri = 'admin/foo/bar';
     
    +        $uri = 'admin/foo/bar';
             $filters->run($uri);
         }
     
    @@ -458,8 +448,8 @@ public function testRunDoesBefore()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri     = 'admin/foo/bar';
             $request = $filters->run($uri, 'before');
     
             $this->assertSame('http://google.com', $request->url);
    @@ -478,8 +468,8 @@ public function testRunDoesAfter()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $response = $filters->run($uri, 'after');
     
             $this->assertSame('http://google.com', $response->csp);
    @@ -498,9 +488,10 @@ public function testShortCircuit()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $response = $filters->run($uri, 'before');
    +
             $this->assertTrue($response instanceof ResponseInterface);
             $this->assertSame('http://google.com', $response->csp);
         }
    @@ -524,8 +515,8 @@ public function testOtherResult()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $response = $filters->run($uri, 'before');
     
             $this->assertSame('This is curious', $response);
    @@ -553,15 +544,14 @@ public function testBeforeExceptString()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $expected = [
                 'before' => [
                     'bar',
                 ],
                 'after' => ['baz'],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -587,8 +577,8 @@ public function testBeforeExceptInapplicable()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $expected = [
                 'before' => [
                     'foo',
    @@ -596,7 +586,6 @@ public function testBeforeExceptInapplicable()
                 ],
                 'after' => ['baz'],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -622,15 +611,14 @@ public function testAfterExceptString()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $expected = [
                 'before' => [
                     'bar',
                 ],
                 'after' => ['baz'],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -656,8 +644,8 @@ public function testAfterExceptInapplicable()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $expected = [
                 'before' => [
                     'bar',
    @@ -667,7 +655,6 @@ public function testAfterExceptInapplicable()
                     'baz',
                 ],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -686,9 +673,7 @@ public function testAddFilter()
             $filters       = new Filters((object) $config, $this->request, $this->response);
     
             $filters = $filters->addFilter('Some\Class', 'some_alias');
    -
             $filters = $filters->initialize('admin/foo/bar');
    -
             $filters = $filters->getFilters();
     
             $this->assertTrue(in_array('some_alias', $filters['before'], true));
    @@ -705,7 +690,6 @@ public function testAddFilterSection()
             $filters = $filters
                 ->addFilter('Some\OtherClass', 'another', 'before', 'globals')
                 ->initialize('admin/foo/bar');
    -
             $list = $filters->getFilters();
     
             $this->assertTrue(in_array('another', $list['before'], true));
    @@ -723,7 +707,6 @@ public function testInitializeTwice()
                 ->addFilter('Some\OtherClass', 'another', 'before', 'globals')
                 ->initialize('admin/foo/bar')
                 ->initialize();
    -
             $list = $filters->getFilters();
     
             $this->assertTrue(in_array('another', $list['before'], true));
    @@ -744,9 +727,7 @@ public function testEnableFilter()
             $filters       = new Filters((object) $config, $this->request, $this->response);
     
             $filters = $filters->initialize('admin/foo/bar');
    -
             $filters->enableFilter('google', 'before');
    -
             $filters = $filters->getFilters();
     
             $this->assertTrue(in_array('google', $filters['before'], true));
    @@ -765,11 +746,10 @@ public function testEnableFilterWithArguments()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $filters       = $filters->initialize('admin/foo/bar');
     
    +        $filters = $filters->initialize('admin/foo/bar');
             $filters->enableFilter('role:admin , super', 'before');
             $filters->enableFilter('role:admin , super', 'after');
    -
             $found = $filters->getFilters();
     
             $this->assertTrue(in_array('role', $found['before'], true));
    @@ -777,9 +757,11 @@ public function testEnableFilterWithArguments()
             $this->assertSame(['role' => ['admin', 'super']], $filters->getArguments());
     
             $response = $filters->run('admin/foo/bar', 'before');
    +
             $this->assertSame('admin;super', $response);
     
             $response = $filters->run('admin/foo/bar', 'after');
    +
             $this->assertSame('admin;super', $response->getBody());
         }
     
    @@ -798,18 +780,18 @@ public function testEnableFilterWithNoArguments()
             $filters       = new Filters((object) $config, $this->request, $this->response);
     
             $filters = $filters->initialize('admin/foo/bar');
    -
             $filters->enableFilter('role', 'before');
             $filters->enableFilter('role', 'after');
    -
             $found = $filters->getFilters();
     
             $this->assertTrue(in_array('role', $found['before'], true));
     
             $response = $filters->run('admin/foo/bar', 'before');
    +
             $this->assertSame('Is null', $response);
     
             $response = $filters->run('admin/foo/bar', 'after');
    +
             $this->assertSame('Is null', $response->getBody());
         }
     
    @@ -830,7 +812,6 @@ public function testEnableNonFilter()
             $filters       = new Filters((object) $config, $this->request, $this->response);
     
             $filters = $filters->initialize('admin/foo/bar');
    -
             $filters->enableFilter('goggle', 'before');
         }
     
    @@ -867,8 +848,8 @@ public function testMatchesURICaseInsensitively()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri      = 'admin/foo/bar';
             $expected = [
                 'before' => [
                     'bar',
    @@ -879,7 +860,6 @@ public function testMatchesURICaseInsensitively()
                     'frak',
                 ],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -905,7 +885,9 @@ public function testFilterMatching()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin';
    +
    +        $uri    = 'admin';
    +        $actual = $filters->initialize($uri)->getFilters();
     
             $expected = [
                 'before' => [
    @@ -913,8 +895,6 @@ public function testFilterMatching()
                 ],
                 'after' => [],
             ];
    -
    -        $actual = $filters->initialize($uri)->getFilters();
             $this->assertSame($expected, $actual);
         }
     
    @@ -944,7 +924,9 @@ public function testGlobalFilterMatching()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin';
    +
    +        $uri    = 'admin';
    +        $actual = $filters->initialize($uri)->getFilters();
     
             $expected = [
                 'before' => [
    @@ -955,8 +937,6 @@ public function testGlobalFilterMatching()
                     'two',
                 ],
             ];
    -
    -        $actual = $filters->initialize($uri)->getFilters();
             $this->assertSame($expected, $actual);
         }
     
    @@ -993,8 +973,8 @@ public function testCombinedFilterMatching()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin123';
     
    +        $uri      = 'admin123';
             $expected = [
                 'before' => [
                     'one',
    @@ -1005,7 +985,6 @@ public function testCombinedFilterMatching()
                     'two',
                 ],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -1039,8 +1018,8 @@ public function testSegmentedFilterMatching()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/123';
     
    +        $uri      = 'admin/123';
             $expected = [
                 'before' => [
                     'frak',
    @@ -1049,7 +1028,6 @@ public function testSegmentedFilterMatching()
                     'frak',
                 ],
             ];
    -
             $this->assertSame($expected, $filters->initialize($uri)->getFilters());
         }
     
    @@ -1073,9 +1051,10 @@ public function testFilterAlitasMultiple()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin/foo/bar';
     
    +        $uri     = 'admin/foo/bar';
             $request = $filters->run($uri, 'before');
    +
             $this->assertSame('http://exampleMultipleURL.com', $request->url);
             $this->assertSame('http://exampleMultipleCSP.com', $request->csp);
         }
    @@ -1099,6 +1078,7 @@ public function testFilterClass()
             $filters       = new Filters((object) $config, $this->request, $this->response);
     
             $filters->run('admin/foo/bar', 'before');
    +
             $expected = [
                 'before' => [],
                 'after'  => [
    @@ -1125,8 +1105,8 @@ public function testReset()
             ];
             $this->request = Services::request();
             $filters       = new Filters((object) $config, $this->request, $this->response);
    -        $uri           = 'admin';
     
    +        $uri = 'admin';
             $this->assertSame(['foo'], $filters->initialize($uri)->getFilters()['before']);
             $this->assertSame([], $filters->reset()->getFilters()['before']);
         }
    
    From 64b37786528de76c3950d3a03b120d24b554d882 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 26 Oct 2021 16:12:42 +0900
    Subject: [PATCH 419/490] test: refactor: extract createFilters() method
    
    ---
     tests/system/Filters/FiltersTest.php | 117 +++++++++++----------------
     1 file changed, 46 insertions(+), 71 deletions(-)
    
    diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
    index 57c3f06753f3..a1e2f0814588 100644
    --- a/tests/system/Filters/FiltersTest.php
    +++ b/tests/system/Filters/FiltersTest.php
    @@ -55,6 +55,13 @@ protected function setUp(): void
             $this->response = Services::response();
         }
     
    +    private function createFilters($config, $request = null): Filters
    +    {
    +        $request = $request ?? Services::request();
    +
    +        return new Filters($config, $request, $this->response);
    +    }
    +
         public function testProcessMethodDetectsCLI()
         {
             $_SERVER['argv'] = [
    @@ -69,8 +76,10 @@ public function testProcessMethodDetectsCLI()
                     'cli' => ['foo'],
                 ],
             ];
    -        $this->request = new CLIRequest(new MockAppConfig());
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters(
    +            (object) $config,
    +            new CLIRequest(new MockAppConfig())
    +        );
     
             $expected = [
                 'before' => ['foo'],
    @@ -89,8 +98,7 @@ public function testProcessMethodDetectsGetRequests()
                     'get' => ['foo'],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $expected = [
                 'before' => ['foo'],
    @@ -113,8 +121,7 @@ public function testProcessMethodRespectsMethod()
                     'get'  => ['bar'],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $expected = [
                 'before' => ['bar'],
    @@ -137,8 +144,7 @@ public function testProcessMethodIgnoresMethod()
                     'get'  => ['bar'],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $expected = [
                 'before' => [],
    @@ -167,8 +173,7 @@ public function testProcessMethodProcessGlobals()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $expected = [
                 'before' => [
    @@ -215,8 +220,7 @@ public function testProcessMethodProcessGlobalsWithExcept(array $except)
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -245,8 +249,7 @@ public function testProcessMethodProcessesFiltersBefore()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -273,8 +276,7 @@ public function testProcessMethodProcessesFiltersAfter()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'users/foo/bar';
             $expected = [
    @@ -319,8 +321,7 @@ public function testProcessMethodProcessesCombined()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -360,8 +361,7 @@ public function testProcessMethodProcessesCombinedAfterForToolbar()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -386,8 +386,7 @@ public function testRunThrowsWithInvalidAlias()
                     'after'  => [],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $this->expectException(FilterException::class);
     
    @@ -406,8 +405,7 @@ public function testCustomFiltersLoad()
                     'after'  => [],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri     = 'admin/foo/bar';
             $request = $filters->run($uri, 'before');
    @@ -426,8 +424,7 @@ public function testRunThrowsWithInvalidClassType()
                     'after'  => [],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $this->expectException(FilterException::class);
     
    @@ -446,8 +443,7 @@ public function testRunDoesBefore()
                     'after'  => [],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri     = 'admin/foo/bar';
             $request = $filters->run($uri, 'before');
    @@ -466,8 +462,7 @@ public function testRunDoesAfter()
                     'after'  => ['google'],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $response = $filters->run($uri, 'after');
    @@ -486,8 +481,7 @@ public function testShortCircuit()
                     'after'  => [],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $response = $filters->run($uri, 'before');
    @@ -513,8 +507,7 @@ public function testOtherResult()
                     'after' => [],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $response = $filters->run($uri, 'before');
    @@ -542,8 +535,7 @@ public function testBeforeExceptString()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -575,8 +567,7 @@ public function testBeforeExceptInapplicable()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -609,8 +600,7 @@ public function testAfterExceptString()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -642,8 +632,7 @@ public function testAfterExceptInapplicable()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -669,8 +658,7 @@ public function testAddFilter()
                     'after'  => [],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $filters = $filters->addFilter('Some\Class', 'some_alias');
             $filters = $filters->initialize('admin/foo/bar');
    @@ -699,9 +687,8 @@ public function testInitializeTwice()
         {
             $_SERVER['REQUEST_METHOD'] = 'GET';
     
    -        $config        = [];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $config  = [];
    +        $filters = $this->createFilters((object) $config);
     
             $filters = $filters
                 ->addFilter('Some\OtherClass', 'another', 'before', 'globals')
    @@ -723,8 +710,7 @@ public function testEnableFilter()
                     'after'  => [],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $filters = $filters->initialize('admin/foo/bar');
             $filters->enableFilter('google', 'before');
    @@ -744,8 +730,7 @@ public function testEnableFilterWithArguments()
                     'after'  => [],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $filters = $filters->initialize('admin/foo/bar');
             $filters->enableFilter('role:admin , super', 'before');
    @@ -776,8 +761,7 @@ public function testEnableFilterWithNoArguments()
                     'after'  => [],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $filters = $filters->initialize('admin/foo/bar');
             $filters->enableFilter('role', 'before');
    @@ -808,8 +792,7 @@ public function testEnableNonFilter()
                     'after'  => [],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $filters = $filters->initialize('admin/foo/bar');
             $filters->enableFilter('goggle', 'before');
    @@ -846,8 +829,7 @@ public function testMatchesURICaseInsensitively()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -883,8 +865,7 @@ public function testFilterMatching()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri    = 'admin';
             $actual = $filters->initialize($uri)->getFilters();
    @@ -922,8 +903,7 @@ public function testGlobalFilterMatching()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri    = 'admin';
             $actual = $filters->initialize($uri)->getFilters();
    @@ -971,8 +951,7 @@ public function testCombinedFilterMatching()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin123';
             $expected = [
    @@ -1016,8 +995,7 @@ public function testSegmentedFilterMatching()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri      = 'admin/123';
             $expected = [
    @@ -1049,8 +1027,7 @@ public function testFilterAlitasMultiple()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri     = 'admin/foo/bar';
             $request = $filters->run($uri, 'before');
    @@ -1074,8 +1051,7 @@ public function testFilterClass()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $filters->run('admin/foo/bar', 'before');
     
    @@ -1103,8 +1079,7 @@ public function testReset()
                     ],
                 ],
             ];
    -        $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filters = $this->createFilters((object) $config);
     
             $uri = 'admin';
             $this->assertSame(['foo'], $filters->initialize($uri)->getFilters()['before']);
    
    From df72c88730dde34778aa73db4c08e7ec51a29ada Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 26 Oct 2021 16:17:19 +0900
    Subject: [PATCH 420/490] test: refactor: use ::class keyword
    
    ---
     tests/system/Filters/FiltersTest.php | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
    index a1e2f0814588..1a7036c29163 100644
    --- a/tests/system/Filters/FiltersTest.php
    +++ b/tests/system/Filters/FiltersTest.php
    @@ -781,7 +781,7 @@ public function testEnableFilterWithNoArguments()
     
         public function testEnableNonFilter()
         {
    -        $this->expectException('CodeIgniter\Filters\Exceptions\FilterException');
    +        $this->expectException(FilterException::class);
     
             $_SERVER['REQUEST_METHOD'] = 'GET';
     
    
    From 908c5627c4410175cf0e38e08acb74a7e7bd021a Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 26 Oct 2021 16:21:12 +0900
    Subject: [PATCH 421/490] test: refactor: remove unused property
    
    ---
     tests/system/Filters/FiltersTest.php | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
    index 1a7036c29163..9ae75c58daee 100644
    --- a/tests/system/Filters/FiltersTest.php
    +++ b/tests/system/Filters/FiltersTest.php
    @@ -34,7 +34,6 @@
      */
     final class FiltersTest extends CIUnitTestCase
     {
    -    protected $request;
         protected $response;
     
         protected function setUp(): void
    
    From 01fc23707987243f3751dfef9c595e96173c865e Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Thu, 28 Oct 2021 10:20:11 +0900
    Subject: [PATCH 422/490] test: refactor: fix wrong config class type
    
    ---
     tests/system/Filters/FiltersTest.php | 148 ++++++++++++++++++++-------
     1 file changed, 109 insertions(+), 39 deletions(-)
    
    diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
    index 9ae75c58daee..8876a3c46956 100644
    --- a/tests/system/Filters/FiltersTest.php
    +++ b/tests/system/Filters/FiltersTest.php
    @@ -17,6 +17,8 @@
     use CodeIgniter\HTTP\ResponseInterface;
     use CodeIgniter\Test\CIUnitTestCase;
     use CodeIgniter\Test\Mock\MockAppConfig;
    +use Config\Filters as FiltersConfig;
    +use LogicException;
     
     require_once __DIR__ . '/fixtures/GoogleMe.php';
     require_once __DIR__ . '/fixtures/GoogleYou.php';
    @@ -54,13 +56,37 @@ protected function setUp(): void
             $this->response = Services::response();
         }
     
    -    private function createFilters($config, $request = null): Filters
    +    private function createFilters(FiltersConfig $config, $request = null): Filters
         {
             $request = $request ?? Services::request();
     
             return new Filters($config, $request, $this->response);
         }
     
    +    /**
    +     * @template T
    +     *
    +     * @param class-string $classname
    +     *
    +     * @return T
    +     */
    +    private function createConfigFromArray(string $classname, array $config)
    +    {
    +        $configObj = new $classname();
    +
    +        foreach ($config as $key => $value) {
    +            if (property_exists($configObj, $key)) {
    +                $configObj->{$key} = $value;
    +            } else {
    +                throw new LogicException(
    +                    'No such property: ' . $classname . '::$' . $key
    +                );
    +            }
    +        }
    +
    +        return $configObj;
    +    }
    +
         public function testProcessMethodDetectsCLI()
         {
             $_SERVER['argv'] = [
    @@ -71,12 +97,14 @@ public function testProcessMethodDetectsCLI()
     
             $config = [
                 'aliases' => ['foo' => ''],
    +            'globals' => [],
                 'methods' => [
                     'cli' => ['foo'],
                 ],
             ];
    -        $filters = $this->createFilters(
    -            (object) $config,
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters(
    +            $filtersConfig,
                 new CLIRequest(new MockAppConfig())
             );
     
    @@ -93,11 +121,13 @@ public function testProcessMethodDetectsGetRequests()
     
             $config = [
                 'aliases' => ['foo' => ''],
    +            'globals' => [],
                 'methods' => [
                     'get' => ['foo'],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $expected = [
                 'before' => ['foo'],
    @@ -115,12 +145,14 @@ public function testProcessMethodRespectsMethod()
                     'foo' => '',
                     'bar' => '',
                 ],
    +            'globals' => [],
                 'methods' => [
                     'post' => ['foo'],
                     'get'  => ['bar'],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $expected = [
                 'before' => ['bar'],
    @@ -138,12 +170,14 @@ public function testProcessMethodIgnoresMethod()
                     'foo' => '',
                     'bar' => '',
                 ],
    +            'globals' => [],
                 'methods' => [
                     'post' => ['foo'],
                     'get'  => ['bar'],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $expected = [
                 'before' => [],
    @@ -172,7 +206,8 @@ public function testProcessMethodProcessGlobals()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $expected = [
                 'before' => [
    @@ -219,7 +254,8 @@ public function testProcessMethodProcessGlobalsWithExcept(array $except)
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -241,6 +277,7 @@ public function testProcessMethodProcessesFiltersBefore()
                     'bar' => '',
                     'baz' => '',
                 ],
    +            'globals' => [],
                 'filters' => [
                     'foo' => [
                         'before' => ['admin/*'],
    @@ -248,7 +285,8 @@ public function testProcessMethodProcessesFiltersBefore()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -268,6 +306,7 @@ public function testProcessMethodProcessesFiltersAfter()
                     'bar' => '',
                     'baz' => '',
                 ],
    +            'globals' => [],
                 'filters' => [
                     'foo' => [
                         'before' => ['admin/*'],
    @@ -275,7 +314,8 @@ public function testProcessMethodProcessesFiltersAfter()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'users/foo/bar';
             $expected = [
    @@ -320,7 +360,8 @@ public function testProcessMethodProcessesCombined()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -360,7 +401,8 @@ public function testProcessMethodProcessesCombinedAfterForToolbar()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -385,7 +427,8 @@ public function testRunThrowsWithInvalidAlias()
                     'after'  => [],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $this->expectException(FilterException::class);
     
    @@ -404,7 +447,8 @@ public function testCustomFiltersLoad()
                     'after'  => [],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri     = 'admin/foo/bar';
             $request = $filters->run($uri, 'before');
    @@ -423,7 +467,8 @@ public function testRunThrowsWithInvalidClassType()
                     'after'  => [],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $this->expectException(FilterException::class);
     
    @@ -442,7 +487,8 @@ public function testRunDoesBefore()
                     'after'  => [],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri     = 'admin/foo/bar';
             $request = $filters->run($uri, 'before');
    @@ -461,7 +507,8 @@ public function testRunDoesAfter()
                     'after'  => ['google'],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $response = $filters->run($uri, 'after');
    @@ -480,7 +527,8 @@ public function testShortCircuit()
                     'after'  => [],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $response = $filters->run($uri, 'before');
    @@ -506,7 +554,8 @@ public function testOtherResult()
                     'after' => [],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $response = $filters->run($uri, 'before');
    @@ -534,7 +583,8 @@ public function testBeforeExceptString()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -566,7 +616,8 @@ public function testBeforeExceptInapplicable()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -599,7 +650,8 @@ public function testAfterExceptString()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -631,7 +683,8 @@ public function testAfterExceptInapplicable()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -657,7 +710,8 @@ public function testAddFilter()
                     'after'  => [],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $filters = $filters->addFilter('Some\Class', 'some_alias');
             $filters = $filters->initialize('admin/foo/bar');
    @@ -672,7 +726,8 @@ public function testAddFilterSection()
     
             $config        = [];
             $this->request = Services::request();
    -        $filters       = new Filters((object) $config, $this->request, $this->response);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = new Filters($filtersConfig, $this->request, $this->response);
     
             $filters = $filters
                 ->addFilter('Some\OtherClass', 'another', 'before', 'globals')
    @@ -686,8 +741,9 @@ public function testInitializeTwice()
         {
             $_SERVER['REQUEST_METHOD'] = 'GET';
     
    -        $config  = [];
    -        $filters = $this->createFilters((object) $config);
    +        $config        = [];
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $filters = $filters
                 ->addFilter('Some\OtherClass', 'another', 'before', 'globals')
    @@ -709,7 +765,8 @@ public function testEnableFilter()
                     'after'  => [],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $filters = $filters->initialize('admin/foo/bar');
             $filters->enableFilter('google', 'before');
    @@ -729,7 +786,8 @@ public function testEnableFilterWithArguments()
                     'after'  => [],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $filters = $filters->initialize('admin/foo/bar');
             $filters->enableFilter('role:admin , super', 'before');
    @@ -760,7 +818,8 @@ public function testEnableFilterWithNoArguments()
                     'after'  => [],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $filters = $filters->initialize('admin/foo/bar');
             $filters->enableFilter('role', 'before');
    @@ -791,7 +850,8 @@ public function testEnableNonFilter()
                     'after'  => [],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $filters = $filters->initialize('admin/foo/bar');
             $filters->enableFilter('goggle', 'before');
    @@ -828,7 +888,8 @@ public function testMatchesURICaseInsensitively()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/foo/bar';
             $expected = [
    @@ -857,6 +918,7 @@ public function testFilterMatching()
                     'bar'  => '',
                     'frak' => '',
                 ],
    +            'globals' => [],
                 'filters' => [
                     'frak' => [
                         'before' => ['admin*'],
    @@ -864,7 +926,8 @@ public function testFilterMatching()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri    = 'admin';
             $actual = $filters->initialize($uri)->getFilters();
    @@ -902,7 +965,8 @@ public function testGlobalFilterMatching()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri    = 'admin';
             $actual = $filters->initialize($uri)->getFilters();
    @@ -950,7 +1014,8 @@ public function testCombinedFilterMatching()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin123';
             $expected = [
    @@ -994,7 +1059,8 @@ public function testSegmentedFilterMatching()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri      = 'admin/123';
             $expected = [
    @@ -1026,7 +1092,8 @@ public function testFilterAlitasMultiple()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri     = 'admin/foo/bar';
             $request = $filters->run($uri, 'before');
    @@ -1050,7 +1117,8 @@ public function testFilterClass()
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $filters->run('admin/foo/bar', 'before');
     
    @@ -1072,13 +1140,15 @@ public function testReset()
                 'aliases' => [
                     'foo' => '',
                 ],
    +            'globals' => [],
                 'filters' => [
                     'foo' => [
                         'before' => ['admin*'],
                     ],
                 ],
             ];
    -        $filters = $this->createFilters((object) $config);
    +        $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $uri = 'admin';
             $this->assertSame(['foo'], $filters->initialize($uri)->getFilters()['before']);
    
    From 1d9c1b6e2b98972a528c9162caa5a6c4e24345a2 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Thu, 28 Oct 2021 10:28:48 +0900
    Subject: [PATCH 423/490] test: refactor: remove $this->request that I forgot
     to remove
    
    ---
     tests/system/Filters/FiltersTest.php | 3 +--
     1 file changed, 1 insertion(+), 2 deletions(-)
    
    diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
    index 8876a3c46956..1ef5e0293c01 100644
    --- a/tests/system/Filters/FiltersTest.php
    +++ b/tests/system/Filters/FiltersTest.php
    @@ -725,9 +725,8 @@ public function testAddFilterSection()
             $_SERVER['REQUEST_METHOD'] = 'GET';
     
             $config        = [];
    -        $this->request = Services::request();
             $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
    -        $filters       = new Filters($filtersConfig, $this->request, $this->response);
    +        $filters       = $this->createFilters($filtersConfig);
     
             $filters = $filters
                 ->addFilter('Some\OtherClass', 'another', 'before', 'globals')
    
    From 24713d43aec82f675e4a510e01343e2cf2b19104 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Thu, 28 Oct 2021 10:33:37 +0900
    Subject: [PATCH 424/490] test: refactor: use method chaining
    
    ---
     tests/system/Filters/FiltersTest.php | 12 ++++++------
     1 file changed, 6 insertions(+), 6 deletions(-)
    
    diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
    index 1ef5e0293c01..7cd014968e3d 100644
    --- a/tests/system/Filters/FiltersTest.php
    +++ b/tests/system/Filters/FiltersTest.php
    @@ -728,10 +728,10 @@ public function testAddFilterSection()
             $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
             $filters       = $this->createFilters($filtersConfig);
     
    -        $filters = $filters
    +        $list = $filters
                 ->addFilter('Some\OtherClass', 'another', 'before', 'globals')
    -            ->initialize('admin/foo/bar');
    -        $list = $filters->getFilters();
    +            ->initialize('admin/foo/bar')
    +            ->getFilters();
     
             $this->assertTrue(in_array('another', $list['before'], true));
         }
    @@ -744,11 +744,11 @@ public function testInitializeTwice()
             $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
             $filters       = $this->createFilters($filtersConfig);
     
    -        $filters = $filters
    +        $list = $filters
                 ->addFilter('Some\OtherClass', 'another', 'before', 'globals')
                 ->initialize('admin/foo/bar')
    -            ->initialize();
    -        $list = $filters->getFilters();
    +            ->initialize()
    +            ->getFilters();
     
             $this->assertTrue(in_array('another', $list['before'], true));
         }
    
    From 3b2fd6ea3144a75092d12293bf55798845227205 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Mon, 1 Nov 2021 09:28:58 +0900
    Subject: [PATCH 425/490] refactor: remove else
    
    ---
     tests/system/Filters/FiltersTest.php | 10 ++++++----
     1 file changed, 6 insertions(+), 4 deletions(-)
    
    diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
    index 7cd014968e3d..20fdf8770863 100644
    --- a/tests/system/Filters/FiltersTest.php
    +++ b/tests/system/Filters/FiltersTest.php
    @@ -77,11 +77,13 @@ private function createConfigFromArray(string $classname, array $config)
             foreach ($config as $key => $value) {
                 if (property_exists($configObj, $key)) {
                     $configObj->{$key} = $value;
    -            } else {
    -                throw new LogicException(
    -                    'No such property: ' . $classname . '::$' . $key
    -                );
    +
    +                continue;
                 }
    +
    +            throw new LogicException(
    +                'No such property: ' . $classname . '::$' . $key
    +            );
             }
     
             return $configObj;
    
    From d1f1f6b2425104a412128331cc3795ec446e1237 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Mon, 1 Nov 2021 14:04:14 +0900
    Subject: [PATCH 426/490] fix: `$this->request->setLocale()` does not work for
     `lang()` in 404 controller
    
    Fixes #5261
    ---
     system/Exceptions/PageNotFoundException.php | 25 +++++++++++++++++----
     1 file changed, 21 insertions(+), 4 deletions(-)
    
    diff --git a/system/Exceptions/PageNotFoundException.php b/system/Exceptions/PageNotFoundException.php
    index 444776c087a2..2a773f1785dc 100644
    --- a/system/Exceptions/PageNotFoundException.php
    +++ b/system/Exceptions/PageNotFoundException.php
    @@ -11,6 +11,7 @@
     
     namespace CodeIgniter\Exceptions;
     
    +use Config\Services;
     use OutOfBoundsException;
     
     class PageNotFoundException extends OutOfBoundsException implements ExceptionInterface
    @@ -26,21 +27,37 @@ class PageNotFoundException extends OutOfBoundsException implements ExceptionInt
     
         public static function forPageNotFound(?string $message = null)
         {
    -        return new static($message ?? lang('HTTP.pageNotFound'));
    +        return new static($message ?? self::lang('HTTP.pageNotFound'));
         }
     
         public static function forEmptyController()
         {
    -        return new static(lang('HTTP.emptyController'));
    +        return new static(self::lang('HTTP.emptyController'));
         }
     
         public static function forControllerNotFound(string $controller, string $method)
         {
    -        return new static(lang('HTTP.controllerNotFound', [$controller, $method]));
    +        return new static(self::lang('HTTP.controllerNotFound', [$controller, $method]));
         }
     
         public static function forMethodNotFound(string $method)
         {
    -        return new static(lang('HTTP.methodNotFound', [$method]));
    +        return new static(self::lang('HTTP.methodNotFound', [$method]));
    +    }
    +
    +    /**
    +     * Get translated system message
    +     *
    +     * Use a non-shared Language instance in the Services.
    +     * If a shared instance is created, the Language will
    +     * have the current locale, so even if users call
    +     * `$this->request->setLocale()` in the controller afterwards,
    +     * the Language locale will not be changed.
    +     */
    +    private static function lang(string $line, array $args = []): string
    +    {
    +        $lang = Services::language(null, false);
    +
    +        return $lang->getLine($line, $args);
         }
     }
    
    From 51966d0c407866f2bdc0dda39a714c0cec8d35aa Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA" 
    Date: Sun, 31 Oct 2021 20:17:00 +0800
    Subject: [PATCH 427/490] Convert bug report to new syntax
    
    ---
     .github/ISSUE_TEMPLATE/bug_report.md  | 40 -----------
     .github/ISSUE_TEMPLATE/bug_report.yml | 99 +++++++++++++++++++++++++++
     2 files changed, 99 insertions(+), 40 deletions(-)
     delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
     create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml
    
    diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
    deleted file mode 100644
    index dc6c9e37de1f..000000000000
    --- a/.github/ISSUE_TEMPLATE/bug_report.md
    +++ /dev/null
    @@ -1,40 +0,0 @@
    ----
    -name: Bug report
    -about: Create a report to help us improve
    -title: 'Bug: '
    -labels: bug
    -assignees: ''
    -
    ----
    -
    ----
    -name: Bug report
    -about: Help us improve the framework by reporting bugs!
    -
    ----
    -
    -**Direction**
    -We use github issues to track bugs, not for support.  
    -If you have a support question, or a feature request, raise these as threads on our
    -[forum](https://forum.codeigniter.com/index.php).
    -
    -**Describe the bug**
    -A clear and concise description of what the bug is.
    -
    -**CodeIgniter 4 version**
    -Which version (and branch, if applicable) the bug is in.
    -
    -**Affected module(s)**
    -Which package or class is the bug in, if known.
    -
    -**Expected behavior, and steps to reproduce if appropriate**
    -A clear and concise description of what you expected to happen,
    -and how you got there.  
    -Feel free to include a text/log extract, but use a pastebin facility for any
    -screenshots you deem necessary.
    -
    -**Context**
    - - OS: [e.g. Windows 99]
    - - Web server: [e.g. Apache 1.2.3]
    - - PHP version: [e.g. 6.5.4]
    - - Database:  [e.g. MySQL]
    diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
    new file mode 100644
    index 000000000000..c20dd368f41a
    --- /dev/null
    +++ b/.github/ISSUE_TEMPLATE/bug_report.yml
    @@ -0,0 +1,99 @@
    +name: Bug report
    +description: Create a report to help us improve CodeIgniter
    +title: "Bug: "
    +labels: ['bug']
    +
    +body:
    +  - type: markdown
    +    attributes:
    +      value: |
    +        Thanks for taking the time to fill out this bug report! Before you begin, please
    +        ensure that there are no existing issues, whether still open or closed, related
    +        to your report. If there is, your report will be closed promptly.
    +
    +  - type: dropdowm
    +    id: php-version
    +    attributes:
    +      label: PHP Version
    +      description: Which PHP versions did you run your code?
    +      multiple: true
    +      options:
    +        - '7.3'
    +        - '7.4'
    +        - '8.0'
    +        - '8.1'
    +    validations:
    +      required: true
    +
    +  - type: input
    +    id: codeigniter-version
    +    attributes:
    +      label: CodeIgniter4 Version
    +    validations:
    +      required: true
    +
    +  - type: dropdown
    +    id: operating-systems
    +    attributes:
    +      label: Which operating systems have you tested for this bug?
    +      description: You may select more than one.
    +      multiple: true
    +      options:
    +        - macOS
    +        - Windows
    +        - Linux
    +    validations:
    +      required: true
    +
    +  - type: dropdown
    +    id: server
    +    attributes:
    +      label: Which server did you use?
    +      options:
    +        - apache
    +        - cli
    +        - cli-server (PHP built-in webserver)
    +        - cgi-fcgi
    +        - fpm-fcgi
    +        - phpdbg
    +    validations:
    +      required: true
    +
    +  - type: input
    +    id: database
    +    attributes:
    +      label: Database
    +    validations:
    +      required: false
    +
    +  - type: textarea
    +    id: description
    +    attributes:
    +      label: What happened?
    +      placeholder: Tell us what you see!
    +    validations:
    +      required: true
    +
    +  - type: textarea
    +    attributes:
    +      label: Steps to Reproduce
    +      description: Steps to reproduce the behavior.
    +    validations:
    +      required: true
    +
    +  - type: textarea
    +    attributes:
    +      label: Expected Output
    +      description: What do you expect to happen instead of this filed bug?
    +    validations:
    +      required: true
    +
    +  - type: textarea
    +    attributes:
    +      label: Anything else?
    +      description: |
    +        Links? References? Anything that will give us more context about the issue you are encountering!
    +
    +        Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
    +    validations:
    +      required: false
    
    From c833671022b89ba29a01c86ee27260d6d8504f35 Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA"
     <51850998+paulbalandan@users.noreply.github.com>
    Date: Mon, 1 Nov 2021 13:36:44 +0800
    Subject: [PATCH 428/490] Fix typo: dropdowm --> dropdown
    
    ---
     .github/ISSUE_TEMPLATE/bug_report.yml | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
    index c20dd368f41a..9536a359300b 100644
    --- a/.github/ISSUE_TEMPLATE/bug_report.yml
    +++ b/.github/ISSUE_TEMPLATE/bug_report.yml
    @@ -11,7 +11,7 @@ body:
             ensure that there are no existing issues, whether still open or closed, related
             to your report. If there is, your report will be closed promptly.
     
    -  - type: dropdowm
    +  - type: dropdown
         id: php-version
         attributes:
           label: PHP Version
    
    From f2ca146bc68332dacbda5f03792c8b94cf88f1c3 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Mon, 1 Nov 2021 16:19:11 +0900
    Subject: [PATCH 429/490] feat: add valid_url_strict rule
    
    Fixes #3156
    ---
     system/Validation/FormatRules.php             | 31 +++++++-
     tests/system/Validation/FormatRulesTest.php   | 71 +++++++++++++++++--
     .../source/libraries/validation.rst           | 10 ++-
     3 files changed, 104 insertions(+), 8 deletions(-)
    
    diff --git a/system/Validation/FormatRules.php b/system/Validation/FormatRules.php
    index 6ee13c3f31df..482765212f0b 100644
    --- a/system/Validation/FormatRules.php
    +++ b/system/Validation/FormatRules.php
    @@ -268,7 +268,10 @@ public function valid_ip(?string $ip = null, ?string $which = null): bool
         }
     
         /**
    -     * Checks a URL to ensure it's formed correctly.
    +     * Checks a string to ensure it is (loosely) a URL.
    +     *
    +     * Warning: this rule will pass basic strings like
    +     * "banana"; use valid_url_strict for a stricter rule.
          *
          * @param string $str
          */
    @@ -291,6 +294,32 @@ public function valid_url(?string $str = null): bool
             return filter_var($str, FILTER_VALIDATE_URL) !== false;
         }
     
    +    /**
    +     * Checks a URL to ensure it's formed correctly.
    +     *
    +     * @param string|null $validSchemes comma separated list of allowed schemes
    +     */
    +    public function valid_url_strict(?string $str = null, ?string $validSchemes = null): bool
    +    {
    +        if (empty($str)) {
    +            return false;
    +        }
    +
    +        if ($validSchemes === null) {
    +            $validSchemes = 'http,https,mailto,tel,sms';
    +        }
    +
    +        $scheme           = strtolower(parse_url($str, PHP_URL_SCHEME));
    +        $validSchemes     = strtolower($validSchemes);
    +        $validSchemeArray = explode(',', $validSchemes);
    +
    +        if (! in_array(($scheme), $validSchemeArray, true)) {
    +            return false;
    +        }
    +
    +        return filter_var($str, FILTER_VALIDATE_URL) !== false;
    +    }
    +
         /**
          * Checks for a valid date and matches a given date format
          *
    diff --git a/tests/system/Validation/FormatRulesTest.php b/tests/system/Validation/FormatRulesTest.php
    index eb457a0c3a2f..30f89e15df94 100644
    --- a/tests/system/Validation/FormatRulesTest.php
    +++ b/tests/system/Validation/FormatRulesTest.php
    @@ -88,7 +88,7 @@ public function testRegexMatchFalse()
         /**
          * @dataProvider urlProvider
          */
    -    public function testValidURL(?string $url, bool $expected)
    +    public function testValidURL(?string $url, bool $isLoose, bool $isStrict)
         {
             $data = [
                 'foo' => $url,
    @@ -98,7 +98,36 @@ public function testValidURL(?string $url, bool $expected)
                 'foo' => 'valid_url',
             ]);
     
    -        $this->assertSame($expected, $this->validation->run($data));
    +        $this->assertSame($isLoose, $this->validation->run($data));
    +    }
    +
    +    /**
    +     * @dataProvider urlProvider
    +     */
    +    public function testValidURLStrict(?string $url, bool $isLoose, bool $isStrict)
    +    {
    +        $data = [
    +            'foo' => $url,
    +        ];
    +
    +        $this->validation->setRules([
    +            'foo' => 'valid_url_strict',
    +        ]);
    +
    +        $this->assertSame($isStrict, $this->validation->run($data));
    +    }
    +
    +    public function testValidURLStrictWithSchema()
    +    {
    +        $data = [
    +            'foo' => 'http://www.codeigniter.com',
    +        ];
    +
    +        $this->validation->setRules([
    +            'foo' => 'valid_url_strict[https]',
    +        ]);
    +
    +        $this->assertFalse($this->validation->run($data));
         }
     
         public function urlProvider()
    @@ -107,61 +136,91 @@ public function urlProvider()
                 [
                     'www.codeigniter.com',
                     true,
    +                false,
                 ],
                 [
                     'http://codeigniter.com',
                     true,
    +                true,
                 ],
    -            //https://bugs.php.net/bug.php?id=51192
    +            // https://bugs.php.net/bug.php?id=51192
                 [
                     'http://accept-dashes.tld',
                     true,
    +                true,
                 ],
                 [
                     'http://reject_underscores',
                     false,
    +                false,
                 ],
    -            // https://github.com/codeigniter4/CodeIgniter/issues/4415
    +            // https://github.com/bcit-ci/CodeIgniter/issues/4415
                 [
                     'http://[::1]/ipv6',
                     true,
    +                true,
                 ],
                 [
                     'htt://www.codeigniter.com',
                     false,
    +                false,
                 ],
                 [
                     '',
                     false,
    +                false,
    +            ],
    +            // https://github.com/codeigniter4/CodeIgniter4/issues/3156
    +            [
    +                'codeigniter',
    +                true,   // What?
    +                false,
                 ],
                 [
                     'code igniter',
                     false,
    +                false,
                 ],
                 [
                     null,
                     false,
    +                false,
                 ],
                 [
                     'http://',
    -                true,
    -            ], // this is apparently valid!
    +                true,   // Why?
    +                false,
    +            ],
                 [
                     'http:///oops.com',
                     false,
    +                false,
                 ],
                 [
                     '123.com',
                     true,
    +                false,
                 ],
                 [
                     'abc.123',
                     true,
    +                false,
                 ],
                 [
                     'http:8080//abc.com',
    +                true,   // Insane?
    +                false,
    +            ],
    +            [
    +                'mailto:support@codeigniter.com',
    +                true,
                     true,
                 ],
    +            [
    +                '//example.com',
    +                false,
    +                false,
    +            ],
             ];
         }
     
    diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst
    index 1779877c50b1..83d22ace13e6 100644
    --- a/user_guide_src/source/libraries/validation.rst
    +++ b/user_guide_src/source/libraries/validation.rst
    @@ -860,7 +860,15 @@ valid_emails            No         Fails if any value provided in a comma
     valid_ip                No         Fails if the supplied IP is not valid.        valid_ip[ipv6]
                                        Accepts an optional parameter of ‘ipv4’ or
                                        ‘ipv6’ to specify an IP format.
    -valid_url               No         Fails if field does not contain a valid URL.
    +valid_url               No         Fails if field does not contain (loosely) a
    +                                   URL. Includes simple strings that could be
    +                                   hostnames, like "codeigniter".
    +valid_url_strict        Yes        Fails if field does not contain a valid URL.  valid_url_strict[http,https]
    +                                   Roughly equivalent to a "fail anything that
    +                                   would not be a clickable link." You can
    +                                   optionally specify a list of valid schemas.
    +                                   If not specified,
    +                                   ``http,https,mailto,tel,sms`` are valid.
     valid_date              No         Fails if field does not contain a valid date. valid_date[d/m/Y]
                                        Accepts an optional parameter to matches
                                        a date format.
    
    From 10b3de20a43759dc38c501948fa742936fd0967a Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Thu, 14 Oct 2021 20:41:45 +0900
    Subject: [PATCH 430/490] docs: make @return strict
    
    ---
     system/Database/BaseBuilder.php | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index 886ce928e132..b26c98189d20 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -1944,7 +1944,7 @@ protected function validateUpdate(): bool
          *
          * @throws DatabaseException
          *
    -     * @return mixed Number of rows affected, SQL string, or FALSE on failure
    +     * @return false|int|string[] Number of rows affected or FALSE on failure, SQL array when testMode
          */
         public function updateBatch(?array $set = null, ?string $index = null, int $batchSize = 100)
         {
    
    From 0f3088c2786c5bc66f39b1857659ca99c47bcdd0 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Thu, 14 Oct 2021 20:46:01 +0900
    Subject: [PATCH 431/490] fix: make the return type of insertBatch() the same
     as updateBatch()
    
    ---
     system/Database/BaseBuilder.php | 7 ++++---
     1 file changed, 4 insertions(+), 3 deletions(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index b26c98189d20..da826824d18d 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -1590,7 +1590,7 @@ public function getWhere($where = null, ?int $limit = null, ?int $offset = 0, bo
          *
          * @throws DatabaseException
          *
    -     * @return false|int Number of rows inserted or FALSE on failure
    +     * @return false|int|string[] Number of rows inserted or FALSE on failure, SQL array when testMode
          */
         public function insertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100)
         {
    @@ -1617,12 +1617,13 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
             $table = $this->QBFrom[0];
     
             $affectedRows = 0;
    +        $savedSQL     = [];
     
             for ($i = 0, $total = count($this->QBSet); $i < $total; $i += $batchSize) {
                 $sql = $this->_insertBatch($this->db->protectIdentifiers($table, true, null, false), $this->QBKeys, array_slice($this->QBSet, $i, $batchSize));
     
                 if ($this->testMode) {
    -                $affectedRows++;
    +                $savedSQL[] = $sql;
                 } else {
                     $this->db->query($sql, $this->binds, false);
                     $affectedRows += $this->db->affectedRows();
    @@ -1633,7 +1634,7 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
                 $this->resetWrite();
             }
     
    -        return $affectedRows;
    +        return $this->testMode ? $savedSQL : $affectedRows;
         }
     
         /**
    
    From c18c70ef655ecc0e48149dcf3c510149e27b1d65 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 15 Oct 2021 10:42:13 +0900
    Subject: [PATCH 432/490] refactor: do not use $this->binds when insertBatch()
    
    To reduce memory consumption.
    ---
     system/Database/BaseBuilder.php | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index da826824d18d..96127360c3fa 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -1625,7 +1625,7 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
                 if ($this->testMode) {
                     $savedSQL[] = $sql;
                 } else {
    -                $this->db->query($sql, $this->binds, false);
    +                $this->db->query($sql, null, false);
                     $affectedRows += $this->db->affectedRows();
                 }
             }
    @@ -1679,7 +1679,7 @@ public function setInsertBatch($key, string $value = '', ?bool $escape = null)
                 $clean = [];
     
                 foreach ($row as $k => $rowValue) {
    -                $clean[] = ':' . $this->setBind($k, $rowValue, $escape) . ':';
    +                $clean[] = $escape ? $this->db->escape($rowValue) : $rowValue;
                 }
     
                 $row = $clean;
    
    From acd566de40d61b78f3a96a86ca711b016cec2832 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 15 Oct 2021 10:59:08 +0900
    Subject: [PATCH 433/490] refactor: reduce memory useage when insertBatch()
     with $set is not null
    
    ---
     system/Database/BaseBuilder.php | 22 ++++++++++++++++++----
     1 file changed, 18 insertions(+), 4 deletions(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index 96127360c3fa..d28cabb570f4 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -1610,17 +1610,27 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
     
                     return false; // @codeCoverageIgnore
                 }
    -
    -            $this->setInsertBatch($set, '', $escape);
             }
     
    +        $hasQBSet = ($set === null);
    +
             $table = $this->QBFrom[0];
     
             $affectedRows = 0;
             $savedSQL     = [];
     
    -        for ($i = 0, $total = count($this->QBSet); $i < $total; $i += $batchSize) {
    -            $sql = $this->_insertBatch($this->db->protectIdentifiers($table, true, null, false), $this->QBKeys, array_slice($this->QBSet, $i, $batchSize));
    +        if ($hasQBSet) {
    +            $set = $this->QBSet;
    +        }
    +
    +        for ($i = 0, $total = count($set); $i < $total; $i += $batchSize) {
    +            if ($hasQBSet) {
    +                $QBSet = array_slice($this->QBSet, $i, $batchSize);
    +            } else {
    +                $this->setInsertBatch(array_slice($set, $i, $batchSize), '', $escape);
    +                $QBSet = $this->QBSet;
    +            }
    +            $sql = $this->_insertBatch($this->db->protectIdentifiers($table, true, null, false), $this->QBKeys, $QBSet);
     
                 if ($this->testMode) {
                     $savedSQL[] = $sql;
    @@ -1628,6 +1638,10 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
                     $this->db->query($sql, null, false);
                     $affectedRows += $this->db->affectedRows();
                 }
    +
    +            if (! $hasQBSet) {
    +                $this->resetWrite();
    +            }
             }
     
             if (! $this->testMode) {
    
    From bc36f0ca3ffa38693e1df49e775a726f4af02467 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 15 Oct 2021 11:10:10 +0900
    Subject: [PATCH 434/490] refactor: resetWrite() when testMode
    
    No need to keep write values even if testMode.
    ---
     system/Database/BaseBuilder.php | 4 +---
     1 file changed, 1 insertion(+), 3 deletions(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index d28cabb570f4..73d9a69b7196 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -1644,9 +1644,7 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
                 }
             }
     
    -        if (! $this->testMode) {
    -            $this->resetWrite();
    -        }
    +        $this->resetWrite();
     
             return $this->testMode ? $savedSQL : $affectedRows;
         }
    
    From adbdd7034293528273905bd8711bbf5c755386ed Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 15 Oct 2021 11:29:28 +0900
    Subject: [PATCH 435/490] refactor: vendor/bin/rector process
    
    ---
     system/Database/BaseBuilder.php | 14 ++++++--------
     1 file changed, 6 insertions(+), 8 deletions(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index 73d9a69b7196..9f0d4888b271 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -1602,14 +1602,12 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
     
                     return false; // @codeCoverageIgnore
                 }
    -        } else {
    -            if (empty($set)) {
    -                if (CI_DEBUG) {
    -                    throw new DatabaseException('insertBatch() called with no data');
    -                }
    -
    -                return false; // @codeCoverageIgnore
    +        } elseif (empty($set)) {
    +            if (CI_DEBUG) {
    +                throw new DatabaseException('insertBatch() called with no data');
                 }
    +
    +            return false; // @codeCoverageIgnore
             }
     
             $hasQBSet = ($set === null);
    @@ -1690,7 +1688,7 @@ public function setInsertBatch($key, string $value = '', ?bool $escape = null)
     
                 $clean = [];
     
    -            foreach ($row as $k => $rowValue) {
    +            foreach ($row as $rowValue) {
                     $clean[] = $escape ? $this->db->escape($rowValue) : $rowValue;
                 }
     
    
    From 8ce784756b39222e1a62faf06304b955a54f5ded Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 15 Oct 2021 11:38:12 +0900
    Subject: [PATCH 436/490] test: remove tests for getOriginalQuery()
    
    Now do not use binds to reduce memory useage.
    ---
     tests/system/Database/Builder/InsertTest.php | 9 ---------
     1 file changed, 9 deletions(-)
    
    diff --git a/tests/system/Database/Builder/InsertTest.php b/tests/system/Database/Builder/InsertTest.php
    index d9c0c79e11e6..2905fe222613 100644
    --- a/tests/system/Database/Builder/InsertTest.php
    +++ b/tests/system/Database/Builder/InsertTest.php
    @@ -91,9 +91,6 @@ public function testInsertBatch()
             $query = $this->db->getLastQuery();
             $this->assertInstanceOf(Query::class, $query);
     
    -        $raw = 'INSERT INTO "jobs" ("description", "id", "name") VALUES (:description:,:id:,:name:), (:description.1:,:id.1:,:name.1:)';
    -        $this->assertSame($raw, str_replace("\n", ' ', $query->getOriginalQuery()));
    -
             $expected = "INSERT INTO \"jobs\" (\"description\", \"id\", \"name\") VALUES ('There''s something in your teeth',2,'Commedian'), ('I am yellow',3,'Cab Driver')";
             $this->assertSame($expected, str_replace("\n", ' ', $query->getQuery()));
         }
    @@ -121,9 +118,6 @@ public function testInsertBatchWithoutEscape()
             $query = $this->db->getLastQuery();
             $this->assertInstanceOf(Query::class, $query);
     
    -        $raw = 'INSERT INTO "jobs" ("description", "id", "name") VALUES (:description:,:id:,:name:), (:description.1:,:id.1:,:name.1:)';
    -        $this->assertSame($raw, str_replace("\n", ' ', $query->getOriginalQuery()));
    -
             $expected = 'INSERT INTO "jobs" ("description", "id", "name") VALUES (1 + 2,2,1 + 1), (2 + 2,3,2 + 1)';
             $this->assertSame($expected, str_replace("\n", ' ', $query->getQuery()));
         }
    @@ -148,9 +142,6 @@ public function testInsertBatchWithFieldsEndingInNumbers()
             $query = $this->db->getLastQuery();
             $this->assertInstanceOf(Query::class, $query);
     
    -        $raw = 'INSERT INTO "ip_table" ("ip", "ip2") VALUES (:ip:,:ip2:), (:ip.1:,:ip2.1:), (:ip.2:,:ip2.2:), (:ip.3:,:ip2.3:)';
    -        $this->assertSame($raw, str_replace("\n", ' ', $query->getOriginalQuery()));
    -
             $expected = "INSERT INTO \"ip_table\" (\"ip\", \"ip2\") VALUES ('1.1.1.0','1.1.1.2'), ('2.2.2.0','2.2.2.2'), ('3.3.3.0','3.3.3.2'), ('4.4.4.0','4.4.4.2')";
             $this->assertSame($expected, str_replace("\n", ' ', $query->getQuery()));
         }
    
    From 516555059753240a1035a77cb88e7cc9f186e9ce Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Fri, 15 Oct 2021 11:41:52 +0900
    Subject: [PATCH 437/490] refactor: do not use $this->binds when updateBatch()
    
    To reduce memory consumption.
    ---
     system/Database/BaseBuilder.php              |  5 ++--
     tests/system/Database/Builder/UpdateTest.php | 26 --------------------
     2 files changed, 2 insertions(+), 29 deletions(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index 9f0d4888b271..58459c37cd4c 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -2076,9 +2076,8 @@ public function setUpdateBatch($key, string $index = '', ?bool $escape = null)
                         $indexSet = true;
                     }
     
    -                $bind = $this->setBind($k2, $v2, $escape);
    -
    -                $clean[$this->db->protectIdentifiers($k2, false)] = ":{$bind}:";
    +                $clean[$this->db->protectIdentifiers($k2, false)]
    +                    = $escape ? $this->db->escape($v2) : $v2;
                 }
     
                 if ($indexSet === false) {
    diff --git a/tests/system/Database/Builder/UpdateTest.php b/tests/system/Database/Builder/UpdateTest.php
    index 1bf36ffe7b3e..bda9ae49915d 100644
    --- a/tests/system/Database/Builder/UpdateTest.php
    +++ b/tests/system/Database/Builder/UpdateTest.php
    @@ -208,19 +208,6 @@ public function testUpdateBatch()
     
             $space = ' ';
     
    -        $expected = <<assertSame($expected, $query->getOriginalQuery());
    -
             $expected = <<assertSame($expected, $query->getOriginalQuery());
    -
             $expected = <<
    Date: Fri, 15 Oct 2021 11:51:49 +0900
    Subject: [PATCH 438/490] refactor: reduce memory useage when updateBatch()
     with $set is not null
    
    ---
     system/Database/BaseBuilder.php | 33 +++++++++++++++++++++++----------
     1 file changed, 23 insertions(+), 10 deletions(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index 58459c37cd4c..b20c4779de34 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -1975,28 +1975,37 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc
     
                     return false; // @codeCoverageIgnore
                 }
    -        } else {
    -            if (empty($set)) {
    -                if (CI_DEBUG) {
    -                    throw new DatabaseException('updateBatch() called with no data');
    -                }
    -
    -                return false; // @codeCoverageIgnore
    +        } elseif (empty($set)) {
    +            if (CI_DEBUG) {
    +                throw new DatabaseException('updateBatch() called with no data');
                 }
     
    -            $this->setUpdateBatch($set, $index);
    +            return false; // @codeCoverageIgnore
             }
     
    +        $hasQBSet = ($set === null);
    +
             $table = $this->QBFrom[0];
     
             $affectedRows = 0;
             $savedSQL     = [];
             $savedQBWhere = $this->QBWhere;
     
    -        for ($i = 0, $total = count($this->QBSet); $i < $total; $i += $batchSize) {
    +        if ($hasQBSet) {
    +            $set = $this->QBSet;
    +        }
    +
    +        for ($i = 0, $total = count($set); $i < $total; $i += $batchSize) {
    +            if ($hasQBSet) {
    +                $QBSet = array_slice($this->QBSet, $i, $batchSize);
    +            } else {
    +                $this->setUpdateBatch(array_slice($set, $i, $batchSize), $index);
    +                $QBSet = $this->QBSet;
    +            }
    +
                 $sql = $this->_updateBatch(
                     $table,
    -                array_slice($this->QBSet, $i, $batchSize),
    +                $QBSet,
                     $this->db->protectIdentifiers($index)
                 );
     
    @@ -2007,6 +2016,10 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc
                     $affectedRows += $this->db->affectedRows();
                 }
     
    +            if (! $hasQBSet) {
    +                $this->resetWrite();
    +            }
    +
                 $this->QBWhere = $savedQBWhere;
             }
     
    
    From 1b7bc3d63d000deca412d737172349e1eb2ffa91 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Mon, 1 Nov 2021 18:42:59 +0900
    Subject: [PATCH 439/490] refactor: remove ()
    
    Co-authored-by: Abdul Malik Ikhsan 
    ---
     system/Database/BaseBuilder.php | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index b20c4779de34..fd71a4afd624 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -1983,7 +1983,7 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc
                 return false; // @codeCoverageIgnore
             }
     
    -        $hasQBSet = ($set === null);
    +        $hasQBSet = $set === null;
     
             $table = $this->QBFrom[0];
     
    
    From 94eaf09a80edaaa04960971c014bf75ad6a87d2b Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Mon, 1 Nov 2021 19:00:14 +0900
    Subject: [PATCH 440/490] test: add test for getOriginalQuery()
    
    ---
     tests/system/Database/Builder/InsertTest.php | 5 +++++
     1 file changed, 5 insertions(+)
    
    diff --git a/tests/system/Database/Builder/InsertTest.php b/tests/system/Database/Builder/InsertTest.php
    index 2905fe222613..5235a3bdbf58 100644
    --- a/tests/system/Database/Builder/InsertTest.php
    +++ b/tests/system/Database/Builder/InsertTest.php
    @@ -91,6 +91,11 @@ public function testInsertBatch()
             $query = $this->db->getLastQuery();
             $this->assertInstanceOf(Query::class, $query);
     
    +        $raw = <<<'SQL'
    +            INSERT INTO "jobs" ("description", "id", "name") VALUES ('There''s something in your teeth',2,'Commedian'), ('I am yellow',3,'Cab Driver')
    +            SQL;
    +        $this->assertSame($raw, str_replace("\n", ' ', $query->getOriginalQuery()));
    +
             $expected = "INSERT INTO \"jobs\" (\"description\", \"id\", \"name\") VALUES ('There''s something in your teeth',2,'Commedian'), ('I am yellow',3,'Cab Driver')";
             $this->assertSame($expected, str_replace("\n", ' ', $query->getQuery()));
         }
    
    From 5ad421a92ed93aff24470411626a4667c13ca7f2 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Mon, 1 Nov 2021 19:01:14 +0900
    Subject: [PATCH 441/490] docs: add breaking changes to changelog
    
    ---
     user_guide_src/source/changelogs/v4.1.5.rst | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/user_guide_src/source/changelogs/v4.1.5.rst b/user_guide_src/source/changelogs/v4.1.5.rst
    index f763123fbd85..b97885f2a3b0 100644
    --- a/user_guide_src/source/changelogs/v4.1.5.rst
    +++ b/user_guide_src/source/changelogs/v4.1.5.rst
    @@ -14,6 +14,8 @@ BREAKING
     
     - Fixed `a bug `_ on CSRF protection. Now CSRF protection works on PUT/PATCH/DELETE requests when CSRF filter is applied. If you use such requests, you need to send CSRF token.
     - In the previous version, if you didn't provide your own headers, ``CURLRequest`` would send the request-headers from the browser, due to a bug. As of this version, it does not send them.
    +- Fixed ``BaseBuilder::insertBatch()`` return value. Now it returns SQL string array instead of wrong affected row number when testMode.
    +- Because of the optimization, when ``BaseBuilder::insertBatch()`` and ``BaseBuilder::updateBatch()`` are used, the return value of ``$query->getOriginalQuery()`` changed.
     
     Enhancements
     ============
    
    From 6e1194f2ee520b88bd90f2e154b9e195ec117e04 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Mon, 1 Nov 2021 19:04:43 +0900
    Subject: [PATCH 442/490] refactor: remove ()
    
    Co-authored-by: Abdul Malik Ikhsan 
    ---
     system/Database/BaseBuilder.php | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
    index fd71a4afd624..74b9dffad0e4 100644
    --- a/system/Database/BaseBuilder.php
    +++ b/system/Database/BaseBuilder.php
    @@ -1610,7 +1610,7 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
                 return false; // @codeCoverageIgnore
             }
     
    -        $hasQBSet = ($set === null);
    +        $hasQBSet = $set === null;
     
             $table = $this->QBFrom[0];
     
    
    From cc5b1d0b6e2319857b1a18e25dd1566d5265ea50 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Mon, 1 Nov 2021 19:52:52 +0900
    Subject: [PATCH 443/490] docs: add breaking changes to upgrade note
    
    ---
     user_guide_src/source/installation/upgrade_415.rst | 8 ++++++++
     1 file changed, 8 insertions(+)
    
    diff --git a/user_guide_src/source/installation/upgrade_415.rst b/user_guide_src/source/installation/upgrade_415.rst
    index 7a8c51f985ae..b6a4007a518b 100644
    --- a/user_guide_src/source/installation/upgrade_415.rst
    +++ b/user_guide_src/source/installation/upgrade_415.rst
    @@ -61,6 +61,14 @@ The bug was fixed. If your requests depend on the headers, your requests might f
     In this case, add the necessary headers manually.
     See `CURLRequest Class <../libraries/curlrequest.html#headers>`_ for how to add.
     
    +Query Builder changes
    +---------------------
    +
    +For optimization and a bug fix, the following behaviors, mostly used in testing, have been changed.
    +
    +- When you use ``insertBatch()`` and ``updateBatch()``, the return value of ``$query->getOriginalQuery()`` changed.
    +- If ``testMode`` is ``true``, ``insertBatch()`` will return an SQL string array instead of the number of affected rows that were wrong.
    +
     Breaking Enhancements
     =====================
     
    
    From 2165b114ada16045106c327bae48407a5043e90b Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA"
     <51850998+paulbalandan@users.noreply.github.com>
    Date: Mon, 1 Nov 2021 23:45:26 +0800
    Subject: [PATCH 444/490] Enclose the PR template's notes as comments
    
    ---
     .github/PULL_REQUEST_TEMPLATE.md | 4 +++-
     1 file changed, 3 insertions(+), 1 deletion(-)
    
    diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
    index 6204913fd437..5b48f145ee3a 100644
    --- a/.github/PULL_REQUEST_TEMPLATE.md
    +++ b/.github/PULL_REQUEST_TEMPLATE.md
    @@ -10,7 +10,7 @@ Explain what you have changed, and why.
     - [ ] User guide updated
     - [ ] Conforms to style guide
     
    ----------Remove from here down in your description----------
    +
    
    From b8c1939cf41652caffa8ef17892938ae1bfab44d Mon Sep 17 00:00:00 2001
    From: "John Paul E. Balandan, CPA"
     <51850998+paulbalandan@users.noreply.github.com>
    Date: Mon, 1 Nov 2021 23:45:54 +0800
    Subject: [PATCH 445/490] Simplify logic of `number_to_roman` function
    
    ---
     system/Helpers/number_helper.php | 91 +++++++++-----------------------
     1 file changed, 24 insertions(+), 67 deletions(-)
    
    diff --git a/system/Helpers/number_helper.php b/system/Helpers/number_helper.php
    index d7f49c15f852..b3d7c80c6006 100644
    --- a/system/Helpers/number_helper.php
    +++ b/system/Helpers/number_helper.php
    @@ -182,79 +182,36 @@ function format_number(float $num, int $precision = 1, ?string $locale = null, a
          */
         function number_to_roman(string $num): ?string
         {
    +        static $map = [
    +            'M'  => 1000,
    +            'CM' => 900,
    +            'D'  => 500,
    +            'CD' => 400,
    +            'C'  => 100,
    +            'XC' => 90,
    +            'L'  => 50,
    +            'XL' => 40,
    +            'X'  => 10,
    +            'IX' => 9,
    +            'V'  => 5,
    +            'IV' => 4,
    +            'I'  => 1,
    +        ];
    +
             $num = (int) $num;
    +
             if ($num < 1 || $num > 3999) {
                 return null;
             }
     
    -        $_number_to_roman = static function ($num, $th) use (&$_number_to_roman) {
    -            $return = '';
    -            $key1   = null;
    -            $key2   = null;
    -
    -            switch ($th) {
    -                case 1:
    -                    $key1 = 'I';
    -                    $key2 = 'V';
    -                    $keyF = 'X';
    -                    break;
    -
    -                case 2:
    -                    $key1 = 'X';
    -                    $key2 = 'L';
    -                    $keyF = 'C';
    -                    break;
    -
    -                case 3:
    -                    $key1 = 'C';
    -                    $key2 = 'D';
    -                    $keyF = 'M';
    -                    break;
    -
    -                case 4:
    -                    $key1 = 'M';
    -                    break;
    -            }
    -            $n = $num % 10;
    -
    -            switch ($n) {
    -                case 1:
    -                case 2:
    -                case 3:
    -                    $return = str_repeat($key1, $n);
    -                    break;
    +        $result = '';
     
    -                case 4:
    -                    $return = $key1 . $key2;
    -                    break;
    -
    -                case 5:
    -                    $return = $key2;
    -                    break;
    -
    -                case 6:
    -                case 7:
    -                case 8:
    -                    $return = $key2 . str_repeat($key1, $n - 5);
    -                    break;
    -
    -                case 9:
    -                    $return = $key1 . $keyF; // @phpstan-ignore-line
    -                    break;
    -            }
    -
    -            switch ($num) {
    -                case 10:
    -                    $return = $keyF; // @phpstan-ignore-line
    -                    break;
    -            }
    -            if ($num > 10) {
    -                $return = $_number_to_roman($num / 10, ++$th) . $return;
    -            }
    -
    -            return $return;
    -        };
    +        foreach ($map as $roman => $arabic) {
    +            $repeat = (int) floor($num / $arabic);
    +            $result .= str_repeat($roman, $repeat);
    +            $num %= $arabic;
    +        }
     
    -        return $_number_to_roman($num, 1);
    +        return $result;
         }
     }
    
    From 056846458554549bd665750fcc7389b75ee9a48c Mon Sep 17 00:00:00 2001
    From: Abdul Malik Ikhsan 
    Date: Mon, 1 Nov 2021 23:10:46 +0700
    Subject: [PATCH 446/490] [Rector] Refactor
     UnderscoreToCamelCaseVariableNameRector so no longer require
     symplify/package-builder
    
    ---
     composer.json                                 |  3 +-
     rector.php                                    |  2 -
     ...nderscoreToCamelCaseVariableNameRector.php | 48 +++++++------------
     3 files changed, 19 insertions(+), 34 deletions(-)
    
    diff --git a/composer.json b/composer.json
    index d914b452e528..b7b3176e055f 100644
    --- a/composer.json
    +++ b/composer.json
    @@ -24,8 +24,7 @@
             "phpstan/phpstan": "^0.12.91",
             "phpunit/phpunit": "^9.1",
             "predis/predis": "^1.1",
    -        "rector/rector": "0.11.60",
    -        "symplify/package-builder": "^9.3"
    +        "rector/rector": "0.11.60"
         },
         "suggest": {
             "ext-fileinfo": "Improves mime type detection for files"
    diff --git a/rector.php b/rector.php
    index 04e761cbf89e..77f8124c7d6f 100644
    --- a/rector.php
    +++ b/rector.php
    @@ -117,8 +117,6 @@
         $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_73);
     
         $services = $containerConfigurator->services();
    -    $services->load('Symplify\\PackageBuilder\\', __DIR__ . '/vendor/symplify/package-builder/src');
    -
         $services->set(UnderscoreToCamelCaseVariableNameRector::class);
         $services->set(SimplifyUselessVariableRector::class);
         $services->set(RemoveAlwaysElseRector::class);
    diff --git a/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php b/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php
    index 005355bb7fd6..f87956f758e4 100644
    --- a/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php
    +++ b/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php
    @@ -13,7 +13,6 @@
     
     namespace Utils\Rector;
     
    -use Nette\Utils\Strings;
     use PhpParser\Comment\Doc;
     use PhpParser\Node;
     use PhpParser\Node\Expr\Variable;
    @@ -21,8 +20,7 @@
     use PhpParser\Node\Stmt\Function_;
     use Rector\Core\Php\ReservedKeywordAnalyzer;
     use Rector\Core\Rector\AbstractRector;
    -use Rector\NodeTypeResolver\Node\AttributeKey;
    -use Symplify\PackageBuilder\Strings\StringFormatConverter;
    +use Rector\NodeNestingScope\ParentFinder;
     use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
     use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
     
    @@ -43,16 +41,16 @@ final class UnderscoreToCamelCaseVariableNameRector extends AbstractRector
         private $reservedKeywordAnalyzer;
     
         /**
    -     * @var StringFormatConverter
    +     * @var ParentFinder
          */
    -    private $stringFormatConverter;
    +    private $parentFinder;
     
         public function __construct(
             ReservedKeywordAnalyzer $reservedKeywordAnalyzer,
    -        StringFormatConverter $stringFormatConverter
    +        ParentFinder $parentFinder
         ) {
             $this->reservedKeywordAnalyzer = $reservedKeywordAnalyzer;
    -        $this->stringFormatConverter   = $stringFormatConverter;
    +        $this->parentFinder            = $parentFinder;
         }
     
         public function getRuleDefinition(): RuleDefinition
    @@ -100,19 +98,20 @@ public function refactor(Node $node): ?Node
                 return null;
             }
     
    -        if (! Strings::contains($nodeName, '_')) {
    -            return null;
    -        }
    -
             if ($this->reservedKeywordAnalyzer->isNativeVariable($nodeName)) {
                 return null;
             }
     
    -        if ($nodeName[0] === '_') {
    +        $underscorePosition = strpos($nodeName, '_');
    +        // underscore not found, or in the first char, skip
    +        if ($underscorePosition === false || $underscorePosition === 0) {
                 return null;
             }
     
    -        $camelCaseName = $this->stringFormatConverter->underscoreAndHyphenToCamelCase($nodeName);
    +        $replaceUnderscoreToSpace = str_replace('_', ' ', $nodeName);
    +        $uppercaseFirstChar       = ucwords($replaceUnderscoreToSpace);
    +        $camelCaseName            = lcfirst(str_replace(' ', '', $uppercaseFirstChar));
    +
             if ($camelCaseName === 'this') {
                 return null;
             }
    @@ -125,24 +124,13 @@ public function refactor(Node $node): ?Node
     
         private function updateDocblock(Variable $variable, string $variableName, string $camelCaseName): void
         {
    -        $parentNode = $variable->getAttribute(AttributeKey::PARENT_NODE);
    -
    -        while ($parentNode) {
    -            /**
    -             * @var ClassMethod|Function_ $parentNode
    -             */
    -            $parentNode = $parentNode->getAttribute(AttributeKey::PARENT_NODE);
    -
    -            if ($parentNode instanceof ClassMethod || $parentNode instanceof Function_) {
    -                break;
    -            }
    -        }
    +        $parentClassMethodOrFunction = $this->parentFinder->findByTypes($variable, [ClassMethod::class, Function_::class]);
     
    -        if ($parentNode === null) {
    +        if ($parentClassMethodOrFunction === null) {
                 return;
             }
     
    -        $docComment = $parentNode->getDocComment();
    +        $docComment = $parentClassMethodOrFunction->getDocComment();
             if ($docComment === null) {
                 return;
             }
    @@ -152,11 +140,11 @@ private function updateDocblock(Variable $variable, string $variableName, string
                 return;
             }
     
    -        if (! Strings::match($docCommentText, sprintf(self::PARAM_NAME_REGEX, $variableName))) {
    +        if (! preg_match(sprintf(self::PARAM_NAME_REGEX, $variableName), $docCommentText)) {
                 return;
             }
     
    -        $phpDocInfo         = $this->phpDocInfoFactory->createFromNodeOrEmpty($parentNode);
    +        $phpDocInfo         = $this->phpDocInfoFactory->createFromNodeOrEmpty($parentClassMethodOrFunction);
             $paramTagValueNodes = $phpDocInfo->getParamTagValueNodes();
     
             foreach ($paramTagValueNodes as $paramTagValueNode) {
    @@ -166,6 +154,6 @@ private function updateDocblock(Variable $variable, string $variableName, string
                 }
             }
     
    -        $parentNode->setDocComment(new Doc($phpDocInfo->getPhpDocNode()->__toString()));
    +        $parentClassMethodOrFunction->setDocComment(new Doc($phpDocInfo->getPhpDocNode()->__toString()));
         }
     }
    
    From efb3b4dd288052061019f44d49bedfc37cb43b3e Mon Sep 17 00:00:00 2001
    From: Abdul Malik Ikhsan 
    Date: Mon, 1 Nov 2021 23:48:11 +0700
    Subject: [PATCH 447/490] cast to int for strpos result
    
    Co-authored-by: John Paul E. Balandan, CPA <51850998+paulbalandan@users.noreply.github.com>
    ---
     utils/Rector/UnderscoreToCamelCaseVariableNameRector.php | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php b/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php
    index f87956f758e4..f87222bb2d5d 100644
    --- a/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php
    +++ b/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php
    @@ -104,7 +104,7 @@ public function refactor(Node $node): ?Node
     
             $underscorePosition = strpos($nodeName, '_');
             // underscore not found, or in the first char, skip
    -        if ($underscorePosition === false || $underscorePosition === 0) {
    +        if ((int) $underscorePosition === 0) {
                 return null;
             }
     
    
    From 9c5f5ccd930fbba7c308837dd6c34ef05e00ca6a Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 2 Nov 2021 08:18:01 +0900
    Subject: [PATCH 448/490] docs: make the description more precise
    
    Co-authored-by: Michal Sniatala 
    ---
     user_guide_src/source/installation/upgrade_415.rst | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/user_guide_src/source/installation/upgrade_415.rst b/user_guide_src/source/installation/upgrade_415.rst
    index b6a4007a518b..ac3cdb529d74 100644
    --- a/user_guide_src/source/installation/upgrade_415.rst
    +++ b/user_guide_src/source/installation/upgrade_415.rst
    @@ -66,7 +66,7 @@ Query Builder changes
     
     For optimization and a bug fix, the following behaviors, mostly used in testing, have been changed.
     
    -- When you use ``insertBatch()`` and ``updateBatch()``, the return value of ``$query->getOriginalQuery()`` changed.
    +- When you use ``insertBatch()`` and ``updateBatch()``, the return value of ``$query->getOriginalQuery()`` has changed. It no longer returns the query with the binded parameters, but the actual query that was run.
     - If ``testMode`` is ``true``, ``insertBatch()`` will return an SQL string array instead of the number of affected rows that were wrong.
     
     Breaking Enhancements
    
    From e08bc930685837000a615b4dd40b67b89d677fae Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 2 Nov 2021 08:20:03 +0900
    Subject: [PATCH 449/490] docs: make the description more specific
    
    Co-authored-by: Michal Sniatala 
    ---
     user_guide_src/source/changelogs/v4.1.5.rst | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/user_guide_src/source/changelogs/v4.1.5.rst b/user_guide_src/source/changelogs/v4.1.5.rst
    index b97885f2a3b0..34eb8937baf4 100644
    --- a/user_guide_src/source/changelogs/v4.1.5.rst
    +++ b/user_guide_src/source/changelogs/v4.1.5.rst
    @@ -15,7 +15,7 @@ BREAKING
     - Fixed `a bug `_ on CSRF protection. Now CSRF protection works on PUT/PATCH/DELETE requests when CSRF filter is applied. If you use such requests, you need to send CSRF token.
     - In the previous version, if you didn't provide your own headers, ``CURLRequest`` would send the request-headers from the browser, due to a bug. As of this version, it does not send them.
     - Fixed ``BaseBuilder::insertBatch()`` return value. Now it returns SQL string array instead of wrong affected row number when testMode.
    -- Because of the optimization, when ``BaseBuilder::insertBatch()`` and ``BaseBuilder::updateBatch()`` are used, the return value of ``$query->getOriginalQuery()`` changed.
    +- Major optimizations have been made to the way data is processed in ``BaseBuilder::insertBatch()`` and ``BaseBuilder::updateBatch()`` methods. This resulted in reduced memory usage and faster query processing. As a trade-off, the result generated by the ``$query->getOriginalQuery()`` method was changed. It no longer returns the query with the binded parameters, but the actual query that was run.
     
     Enhancements
     ============
    
    From 019ea5acadf7993c0ddaf72c605ac220b709bf88 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 2 Nov 2021 08:36:12 +0900
    Subject: [PATCH 450/490] refactor: remove if statements
    
    ---
     system/Validation/FormatRules.php | 22 +++++++++-------------
     1 file changed, 9 insertions(+), 13 deletions(-)
    
    diff --git a/system/Validation/FormatRules.php b/system/Validation/FormatRules.php
    index 482765212f0b..5f6289b41c4c 100644
    --- a/system/Validation/FormatRules.php
    +++ b/system/Validation/FormatRules.php
    @@ -305,19 +305,15 @@ public function valid_url_strict(?string $str = null, ?string $validSchemes = nu
                 return false;
             }
     
    -        if ($validSchemes === null) {
    -            $validSchemes = 'http,https,mailto,tel,sms';
    -        }
    -
    -        $scheme           = strtolower(parse_url($str, PHP_URL_SCHEME));
    -        $validSchemes     = strtolower($validSchemes);
    -        $validSchemeArray = explode(',', $validSchemes);
    -
    -        if (! in_array(($scheme), $validSchemeArray, true)) {
    -            return false;
    -        }
    -
    -        return filter_var($str, FILTER_VALIDATE_URL) !== false;
    +        $scheme       = strtolower(parse_url($str, PHP_URL_SCHEME));
    +        $validSchemes = explode(
    +            ',',
    +            strtolower($validSchemes ?? 'http,https,mailto,tel,sms')
    +        );
    +
    +        return ! in_array($scheme, $validSchemes, true)
    +            ? false
    +            : filter_var($str, FILTER_VALIDATE_URL) !== false;
         }
     
         /**
    
    From 5b91b08731b90d907a859d3578a1e173a029ffca Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 2 Nov 2021 09:02:55 +0900
    Subject: [PATCH 451/490] docs: add how to fix code with Rector
    
    ---
     contributing/pull_request.md | 4 ++++
     1 file changed, 4 insertions(+)
    
    diff --git a/contributing/pull_request.md b/contributing/pull_request.md
    index d08bbd2e035c..e08a5c110558 100644
    --- a/contributing/pull_request.md
    +++ b/contributing/pull_request.md
    @@ -199,6 +199,10 @@ Rector, on the other hand, can be run on the specific files you modified or adde
     
     	vendor/bin/rector process --dry-run path/to/file
     
    +If you run it without `--dry-run`, Rector will fix the code:
    +
    +	vendor/bin/rector process path/to/file
    +
     [1]: https://github.com/phpstan/phpstan-src
     [2]: https://github.com/rector/rector
     
    
    From 1b0850e422568f4527b6c70247e697d83817cea2 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 2 Nov 2021 09:11:04 +0900
    Subject: [PATCH 452/490] refactor: simplify return statement
    
    ---
     system/Validation/FormatRules.php | 5 ++---
     1 file changed, 2 insertions(+), 3 deletions(-)
    
    diff --git a/system/Validation/FormatRules.php b/system/Validation/FormatRules.php
    index 5f6289b41c4c..daf5061c80f6 100644
    --- a/system/Validation/FormatRules.php
    +++ b/system/Validation/FormatRules.php
    @@ -311,9 +311,8 @@ public function valid_url_strict(?string $str = null, ?string $validSchemes = nu
                 strtolower($validSchemes ?? 'http,https,mailto,tel,sms')
             );
     
    -        return ! in_array($scheme, $validSchemes, true)
    -            ? false
    -            : filter_var($str, FILTER_VALIDATE_URL) !== false;
    +        return in_array($scheme, $validSchemes, true)
    +            && filter_var($str, FILTER_VALIDATE_URL) !== false;
         }
     
         /**
    
    From eeaa4db60a2fd7fdb7daa7bcfb1fe73446e6e200 Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 2 Nov 2021 15:37:45 +0900
    Subject: [PATCH 453/490] docs: move CSRF Protection Methods section up
    
    This is a basic config item, so it is better to have it at the beginning.
    ---
     user_guide_src/source/libraries/security.rst | 31 ++++++++++----------
     1 file changed, 15 insertions(+), 16 deletions(-)
    
    diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst
    index 48b97baf1f61..a7ee6e141f78 100644
    --- a/user_guide_src/source/libraries/security.rst
    +++ b/user_guide_src/source/libraries/security.rst
    @@ -26,6 +26,21 @@ Cross-site request forgery (CSRF)
     .. warning:: The CSRF Protection is only available for **POST/PUT/PATCH/DELETE** requests.
         Requests for other methods are not protected.
     
    +CSRF Protection Methods
    +=======================
    +
    +By default, the Cookie based CSRF Protection is used. It is
    +`Double Submit Cookie `_
    +on OWASP Cross-Site Request Forgery Prevention Cheat Sheet.
    +
    +You can also use Session based CSRF Protection. It is
    +`Synchronizer Token Pattern `_.
    +
    +You can set to use the Session based CSRF protection by editing the following config parameter value in
    +**app/Config/Security.php**::
    +
    +    public $csrfProtection = 'session';
    +
     Enable CSRF Protection
     ======================
     
    @@ -130,22 +145,6 @@ than simply crashing. This can be turned off by editing the following config par
     
     Even when the redirect value is ``true``, AJAX calls will not redirect, but will throw an error.
     
    -=======================
    -CSRF Protection Methods
    -=======================
    -
    -By default, the Cookie based CSRF Protection is used. It is
    -`Double Submit Cookie `_
    -on OWASP Cross-Site Request Forgery Prevention Cheat Sheet.
    -
    -You can also use Session based CSRF Protection. It is
    -`Synchronizer Token Pattern `_.
    -
    -You can set to use the Session based CSRF protection by editing the following config parameter value in
    -**app/Config/Security.php**::
    -
    -    public $csrfProtection = 'session';
    -
     *********************
     Other Helpful Methods
     *********************
    
    From 8ffd0ad3895b4d16f77817f25ba79f4e943549fc Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 2 Nov 2021 15:44:35 +0900
    Subject: [PATCH 454/490] docs: fix sample code format
    
    ---
     user_guide_src/source/helpers/html_helper.rst | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/user_guide_src/source/helpers/html_helper.rst b/user_guide_src/source/helpers/html_helper.rst
    index a477b9cbd311..a7ad04914831 100755
    --- a/user_guide_src/source/helpers/html_helper.rst
    +++ b/user_guide_src/source/helpers/html_helper.rst
    @@ -79,7 +79,7 @@ The following functions are available:
             echo img($src);
     
         There is an optional second parameter to specify the MIME type, otherwise the
    -    function will use your Mimes config to guess.
    +    function will use your Mimes config to guess::
     
             $src = img_data('path/img_without_extension', 'image/png'); // data:image/png;base64,HT5A822...
     
    
    From 0705c535247a13a0ede50b53cea53d545464123e Mon Sep 17 00:00:00 2001
    From: kenjis 
    Date: Tue, 2 Nov 2021 15:47:50 +0900
    Subject: [PATCH 455/490] docs: decorate HTML tags with '``'
    
    ---
     user_guide_src/source/helpers/html_helper.rst | 20 +++++++++----------
     1 file changed, 10 insertions(+), 10 deletions(-)
    
    diff --git a/user_guide_src/source/helpers/html_helper.rst b/user_guide_src/source/helpers/html_helper.rst
    index a7ad04914831..8eb55fb649d0 100755
    --- a/user_guide_src/source/helpers/html_helper.rst
    +++ b/user_guide_src/source/helpers/html_helper.rst
    @@ -32,7 +32,7 @@ The following functions are available:
         :returns:   HTML image tag
         :rtype: string
     
    -    Lets you create HTML  tags. The first parameter contains the
    +    Lets you create HTML ```` tags. The first parameter contains the
         image source. Example::
     
             echo img('images/picture.jpg');
    @@ -99,7 +99,7 @@ The following functions are available:
         :returns:   HTML link tag
         :rtype: string
     
    -    Lets you create HTML  tags. This is useful for stylesheet links,
    +    Lets you create HTML ```` tags. This is useful for stylesheet links,
         as well as other links. The parameters are *href*, with optional *rel*,
         *type*, *title*, *media* and *indexPage*.
     
    @@ -139,7 +139,7 @@ The following functions are available:
         :returns:   HTML script tag
         :rtype: string
     
    -    Lets you create HTML  tags. The parameters is *src*, with optional *indexPage*.
    +    Lets you create HTML ```` tags. The parameters is *src*, with optional *indexPage*.
     
         *indexPage* is a boolean value that specifies if the *src* should have
         the page specified by ``$config['indexPage']`` added to the address it creates.
    @@ -284,8 +284,8 @@ The following functions are available:
         :returns:   HTML-formatted ordered list
         :rtype: string
     
    -    Identical to :php:func:`ul()`, only it produces the 
      tag for - ordered lists instead of
        . + Identical to :php:func:`ul()`, only it produces the ``
          `` tag for + ordered lists instead of ``
            ``. .. php:function:: video($src[, $unsupportedMessage = ''[, $attributes = ''[, $tracks = [][, $indexPage = false]]]]) @@ -359,7 +359,7 @@ The following functions are available: :returns: HTML-formatted audio element :rtype: string - Identical to :php:func:`video()`, only it produces the