From 1ade2e72da6757e8702fac9c461445be3d70330f Mon Sep 17 00:00:00 2001 From: Add-ngr Date: Sun, 15 Jan 2023 21:26:39 +0530 Subject: [PATCH 001/485] feat: `renderSection` option to retained data --- system/View/View.php | 9 +++++++-- tests/system/View/ViewTest.php | 10 ++++++++++ .../system/View/Views/extend_reuse_section.php | 9 +++++++++ tests/system/View/Views/layout_welcome.php | 3 +++ user_guide_src/source/outgoing/view_layouts.rst | 17 +++++++++++++++-- 5 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 tests/system/View/Views/extend_reuse_section.php create mode 100644 tests/system/View/Views/layout_welcome.php diff --git a/system/View/View.php b/system/View/View.php index 9a39104728df..842c9993414b 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -406,8 +406,11 @@ public function endSection() /** * Renders a section's contents. + * + * @param bool $saveData If true, saves data for subsequent calls, + * if false, cleans the data after displaying. */ - public function renderSection(string $sectionName) + public function renderSection(string $sectionName, bool $saveData = false) { if (! isset($this->sections[$sectionName])) { echo ''; @@ -417,7 +420,9 @@ public function renderSection(string $sectionName) foreach ($this->sections[$sectionName] as $key => $contents) { echo $contents; - unset($this->sections[$sectionName][$key]); + if ($saveData === false) { + unset($this->sections[$sectionName][$key]); + } } } diff --git a/tests/system/View/ViewTest.php b/tests/system/View/ViewTest.php index 537740231f66..11b823c80fed 100644 --- a/tests/system/View/ViewTest.php +++ b/tests/system/View/ViewTest.php @@ -387,4 +387,14 @@ public function testRenderNestedSections() $this->assertStringContainsString('

Second

', $content); $this->assertStringContainsString('

Third

', $content); } + + public function testRenderSectionSavingData() + { + $view = new View($this->config, $this->viewsDir, $this->loader); + $expected = "Welcome to CodeIgniter 4!\n

Welcome to CodeIgniter 4!

\n

Hello World

"; + + $view->setVar('pageTitle', 'Welcome to CodeIgniter 4!'); + $view->setVar('testString', 'Hello World'); + $this->assertSame($expected, $view->render('extend_reuse_section')); + } } diff --git a/tests/system/View/Views/extend_reuse_section.php b/tests/system/View/Views/extend_reuse_section.php new file mode 100644 index 000000000000..f7a5c0a263e2 --- /dev/null +++ b/tests/system/View/Views/extend_reuse_section.php @@ -0,0 +1,9 @@ +extend('layout_welcome') ?> + +section('page_title') ?> + +endSection() ?> + +section('content') ?> + +endSection() ?> diff --git a/tests/system/View/Views/layout_welcome.php b/tests/system/View/Views/layout_welcome.php new file mode 100644 index 000000000000..0eebf376fb7e --- /dev/null +++ b/tests/system/View/Views/layout_welcome.php @@ -0,0 +1,3 @@ +<?= $this->renderSection('page_title', true) ?> +

renderSection('page_title') ?>

+

renderSection('content') ?>

\ No newline at end of file diff --git a/user_guide_src/source/outgoing/view_layouts.rst b/user_guide_src/source/outgoing/view_layouts.rst index 2600b1c08922..daa326c65a01 100644 --- a/user_guide_src/source/outgoing/view_layouts.rst +++ b/user_guide_src/source/outgoing/view_layouts.rst @@ -31,8 +31,21 @@ E.g. **app/Views/default.php**:: -The ``renderSection()`` method only has one argument - the name of the section. That way any child views know -what to name the content section. +The ``renderSection()`` method have two arguments ``$sectionName`` - the name of the section. That way any child views know +what to name the content section. And ``$saveData`` - If true, saves data for subsequent calls, if false, cleans the data after displaying. + +E.g. **app/Views/welcome_message.php**:: + + + + + <?= $this->renderSection('page_title', true) ?> + + +

renderSection('page_title') ?>

+

renderSection('content') ?>

+ + ********************** Using Layouts in Views From fb34e69b54919fd58dd381b43e602050815e167f Mon Sep 17 00:00:00 2001 From: Add-ngr Date: Mon, 16 Jan 2023 23:09:28 +0530 Subject: [PATCH 002/485] changelog v4.4.0 added --- user_guide_src/source/changelogs/v4.4.0.rst | 78 +++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 user_guide_src/source/changelogs/v4.4.0.rst diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst new file mode 100644 index 000000000000..bb7579e4c48d --- /dev/null +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -0,0 +1,78 @@ +Version 4.4.0 +############# + +Release Date: Unreleased + +**4.4.0 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 3 + +Highlights +********** + +- TBD + +BREAKING +******** + +Behavior Changes +================ + +Interface Changes +================= + +Method Signature Changes +======================== + +Enhancements +************ + +Commands +======== + +Testing +======= + +Database +======== + +Query Builder +------------- + +Forge +----- + +Others +------ + +Model +===== + +Libraries +========= + +Helpers and Functions +===================== + +Others +====== +- **View:** Added optional 2nd argument on ``renderSection('page_title', true)`` to prevent from auto cleans the data after displaying. See ([#7126](https://github.com/codeigniter4/CodeIgniter4/pull/7126)) for details. + + +Message Changes +*************** + +Changes +******* + +Deprecations +************ + +Bugs Fixed +********** + +See the repo's +`CHANGELOG.md `_ +for a complete list of bugs fixed. From becb4806dde2aec9447ce8f45d802d5df8570f98 Mon Sep 17 00:00:00 2001 From: Add-ngr Date: Wed, 18 Jan 2023 23:55:56 +0530 Subject: [PATCH 003/485] feat: renderSection option to retained data, after 1st review change --- tests/system/View/ViewTest.php | 2 +- tests/system/View/Views/layout_welcome.php | 2 +- user_guide_src/source/changelogs/index.rst | 1 + user_guide_src/source/changelogs/v4.4.0.rst | 2 +- user_guide_src/source/outgoing/view_layouts.rst | 7 +++++-- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/system/View/ViewTest.php b/tests/system/View/ViewTest.php index 11b823c80fed..61742d943984 100644 --- a/tests/system/View/ViewTest.php +++ b/tests/system/View/ViewTest.php @@ -395,6 +395,6 @@ public function testRenderSectionSavingData() $view->setVar('pageTitle', 'Welcome to CodeIgniter 4!'); $view->setVar('testString', 'Hello World'); - $this->assertSame($expected, $view->render('extend_reuse_section')); + $this->assertStringContainsString($expected, $view->render('extend_reuse_section')); } } diff --git a/tests/system/View/Views/layout_welcome.php b/tests/system/View/Views/layout_welcome.php index 0eebf376fb7e..a3623a4aafad 100644 --- a/tests/system/View/Views/layout_welcome.php +++ b/tests/system/View/Views/layout_welcome.php @@ -1,3 +1,3 @@ <?= $this->renderSection('page_title', true) ?>

renderSection('page_title') ?>

-

renderSection('content') ?>

\ No newline at end of file +

renderSection('content') ?>

diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index 9932e8725891..cdffd1cb94f0 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.4.0 v4.3.2 v4.3.1 v4.3.0 diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index bb7579e4c48d..0203732d15e5 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -58,7 +58,7 @@ Helpers and Functions Others ====== -- **View:** Added optional 2nd argument on ``renderSection('page_title', true)`` to prevent from auto cleans the data after displaying. See ([#7126](https://github.com/codeigniter4/CodeIgniter4/pull/7126)) for details. +- **View:** Added optional 2nd argument on ``renderSection('page_title', true)`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. Message Changes diff --git a/user_guide_src/source/outgoing/view_layouts.rst b/user_guide_src/source/outgoing/view_layouts.rst index daa326c65a01..f6292d4b95c8 100644 --- a/user_guide_src/source/outgoing/view_layouts.rst +++ b/user_guide_src/source/outgoing/view_layouts.rst @@ -31,8 +31,9 @@ E.g. **app/Views/default.php**:: -The ``renderSection()`` method have two arguments ``$sectionName`` - the name of the section. That way any child views know -what to name the content section. And ``$saveData`` - If true, saves data for subsequent calls, if false, cleans the data after displaying. +The ``renderSection()`` method has two arguments: ``$sectionName`` and ``$saveData``. ``$sectionName`` is the name of +the section used by any child view to name the content section. If the boolean argument ``$saveData`` is set to true, +the method saves data for subsequent calls. Otherwise, the method cleans the data after displaying the contents. E.g. **app/Views/welcome_message.php**:: @@ -47,6 +48,8 @@ E.g. **app/Views/welcome_message.php**:: +.. note:: ``$saveData`` can be used since v4.4.0. + ********************** Using Layouts in Views ********************** From 40d649df4377933bb4f39c9aa244a3af4fc411e1 Mon Sep 17 00:00:00 2001 From: Add-ngr Date: Thu, 19 Jan 2023 08:50:11 +0530 Subject: [PATCH 004/485] changelog updated in v4.4.0 --- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 0203732d15e5..6632a061c4d1 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -58,7 +58,7 @@ Helpers and Functions Others ====== -- **View:** Added optional 2nd argument on ``renderSection('page_title', true)`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. +- **View:** Added optional 2nd parameter ``$saveData`` on ``renderSection()`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. Message Changes From 29ebd6736727570b7d6045055872874461d2a386 Mon Sep 17 00:00:00 2001 From: Add-ngr Date: Thu, 19 Jan 2023 20:01:13 +0530 Subject: [PATCH 005/485] defined a label on **Creating A Layout** section --- user_guide_src/source/outgoing/view_layouts.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/outgoing/view_layouts.rst b/user_guide_src/source/outgoing/view_layouts.rst index f6292d4b95c8..6e6c672710fd 100644 --- a/user_guide_src/source/outgoing/view_layouts.rst +++ b/user_guide_src/source/outgoing/view_layouts.rst @@ -12,6 +12,8 @@ any view being rendered. You could create different layouts to support one-colum blog archive pages, and more. Layouts are never directly rendered. Instead, you render a view, which specifies the layout that it wants to extend. +.. _creating-a-layout: + ***************** Creating A Layout ***************** From abe19f0fb7d38032219308e587d2eca91fd727b6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 22 Jan 2023 12:50:40 +0900 Subject: [PATCH 006/485] docs: improve comments --- system/Router/AutoRouterImproved.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 39b264ecf780..365b91c96ea6 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -168,13 +168,13 @@ public function getRoute(string $uri): array '\\' ); - // Ensure routes registered via $routes->cli() are not accessible via web. + // Ensure the controller is not defined in routes. $this->protectDefinedRoutes(); - // Check _remap() + // Ensure the controller does not have _remap() method. $this->checkRemap(); - // Check parameters + // Check parameter count try { $this->checkParameters($uri); } catch (ReflectionException $e) { From a82b4dc0dbab279db9dd2922c304a524a77a409c Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 22 Jan 2023 14:07:24 +0900 Subject: [PATCH 007/485] feat: fallback to default method --- system/Router/AutoRouterImproved.php | 35 +++++++++++++++---- .../Exceptions/MethodNotFoundException.php | 21 +++++++++++ .../system/Router/AutoRouterImprovedTest.php | 13 +++++++ tests/system/Router/Controllers/Index.php | 2 +- 4 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 system/Router/Exceptions/MethodNotFoundException.php diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 365b91c96ea6..87409e78f107 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Router; use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\Router\Exceptions\MethodNotFoundException; use ReflectionClass; use ReflectionException; @@ -177,8 +178,21 @@ public function getRoute(string $uri): array // Check parameter count try { $this->checkParameters($uri); - } catch (ReflectionException $e) { - throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); + } catch (MethodNotFoundException $e) { + // Fallback to the default method + if (! isset($methodSegment)) { + throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); + } + + array_unshift($this->params, $methodSegment); + $method = $this->method; + $this->method = $this->defaultMethod; + + try { + $this->checkParameters($uri); + } catch (MethodNotFoundException $e) { + throw PageNotFoundException::forControllerNotFound($this->controller, $method); + } } return [$this->directory, $this->controller, $this->method, $this->params]; @@ -201,12 +215,21 @@ private function protectDefinedRoutes(): void private function checkParameters(string $uri): void { - $refClass = new ReflectionClass($this->controller); - $refMethod = $refClass->getMethod($this->method); - $refParams = $refMethod->getParameters(); + try { + $refClass = new ReflectionClass($this->controller); + } catch (ReflectionException $e) { + throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); + } + + try { + $refMethod = $refClass->getMethod($this->method); + $refParams = $refMethod->getParameters(); + } catch (ReflectionException $e) { + throw new MethodNotFoundException(); + } if (! $refMethod->isPublic()) { - throw PageNotFoundException::forMethodNotFound($this->method); + throw new MethodNotFoundException(); } if (count($refParams) < count($this->params)) { diff --git a/system/Router/Exceptions/MethodNotFoundException.php b/system/Router/Exceptions/MethodNotFoundException.php new file mode 100644 index 000000000000..d9ad45a5fa3d --- /dev/null +++ b/system/Router/Exceptions/MethodNotFoundException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Router\Exceptions; + +use RuntimeException; + +/** + * @internal + */ +final class MethodNotFoundException extends RuntimeException +{ +} diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index ec94676be121..ab1fa7db2188 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -199,6 +199,19 @@ public function testAutoRouteFindsDefaultDashFolder() $this->assertSame([], $params); } + public function testAutoRouteFallbackToDefaultMethod() + { + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute('index/15'); + + $this->assertNull($directory); + $this->assertSame('\\' . Index::class, $controller); + $this->assertSame('getIndex', $method); + $this->assertSame(['15'], $params); + } + public function testAutoRouteRejectsSingleDot() { $this->expectException(PageNotFoundException::class); diff --git a/tests/system/Router/Controllers/Index.php b/tests/system/Router/Controllers/Index.php index bfc3539e0b1f..2ad043942923 100644 --- a/tests/system/Router/Controllers/Index.php +++ b/tests/system/Router/Controllers/Index.php @@ -15,7 +15,7 @@ class Index extends Controller { - public function getIndex() + public function getIndex($p1 = '') { } From 63677bbb2bf106a50efc31cc5712271db53b677d Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 22 Jan 2023 14:08:29 +0900 Subject: [PATCH 008/485] feat: show parameters of default method --- .../ControllerMethodReader.php | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php index 3f373c433f1b..aaa0986df345 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php @@ -76,18 +76,37 @@ public function read(string $class, string $defaultController = 'Home', string $ ); if ($routeWithoutController !== []) { + // Route for the default controller. $output = [...$output, ...$routeWithoutController]; continue; } + $params = []; + $routeParams = ''; + $refParams = $method->getParameters(); + + foreach ($refParams as $param) { + $required = true; + if ($param->isOptional()) { + $required = false; + + $routeParams .= '[/..]'; + } else { + $routeParams .= '/..'; + } + + // [variable_name => required?] + $params[$param->getName()] = $required; + } + // Route for the default method. $output[] = [ 'method' => $httpVerb, 'route' => $classInUri, - 'route_params' => '', + 'route_params' => $routeParams, 'handler' => '\\' . $classname . '::' . $methodName, - 'params' => [], + 'params' => $params, ]; continue; From d93340ea908d868c660bc467dcf84d65e83e8cb3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 22 Jan 2023 14:14:32 +0900 Subject: [PATCH 009/485] test: update tests --- tests/_support/Controllers/Newautorouting.php | 2 +- tests/system/Commands/RoutesTest.php | 2 +- .../Routes/AutoRouterImproved/AutoRouteCollectorTest.php | 4 ++-- .../AutoRouterImproved/ControllerMethodReaderTest.php | 6 ++++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/_support/Controllers/Newautorouting.php b/tests/_support/Controllers/Newautorouting.php index f3c8479f3685..2c31547ed2de 100644 --- a/tests/_support/Controllers/Newautorouting.php +++ b/tests/_support/Controllers/Newautorouting.php @@ -15,7 +15,7 @@ class Newautorouting extends Controller { - public function getIndex() + public function getIndex(string $m = '') { return 'Hello'; } diff --git a/tests/system/Commands/RoutesTest.php b/tests/system/Commands/RoutesTest.php index 6d506274145d..8567dc6bc70a 100644 --- a/tests/system/Commands/RoutesTest.php +++ b/tests/system/Commands/RoutesTest.php @@ -129,7 +129,7 @@ public function testRoutesCommandAutoRouteImproved() | TRACE | testing | testing-index | \App\Controllers\TestController::index | | toolbar | | CONNECT | testing | testing-index | \App\Controllers\TestController::index | | toolbar | | CLI | testing | testing-index | \App\Controllers\TestController::index | | | - | GET(auto) | newautorouting | | \Tests\Support\Controllers\Newautorouting::getIndex | | toolbar | + | GET(auto) | newautorouting[/..] | | \Tests\Support\Controllers\Newautorouting::getIndex | | toolbar | | POST(auto) | newautorouting/save/../..[/..] | | \Tests\Support\Controllers\Newautorouting::postSave | | toolbar | +------------+--------------------------------+---------------+-----------------------------------------------------+----------------+---------------+ EOL; diff --git a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollectorTest.php b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollectorTest.php index 4b99de7dbb07..72ebc1cf336a 100644 --- a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollectorTest.php +++ b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollectorTest.php @@ -62,7 +62,7 @@ public function testGetFilterMatches() $expected = [ 0 => [ 'GET(auto)', - 'newautorouting', + 'newautorouting[/..]', '', '\\Tests\\Support\\Controllers\\Newautorouting::getIndex', '', @@ -90,7 +90,7 @@ public function testGetFilterDoesNotMatch() $expected = [ 0 => [ 'GET(auto)', - 'newautorouting', + 'newautorouting[/..]', '', '\\Tests\\Support\\Controllers\\Newautorouting::getIndex', '', diff --git a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php index 6a77f0173ab5..6dd81f4722f2 100644 --- a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php +++ b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php @@ -43,9 +43,11 @@ public function testRead() 0 => [ 'method' => 'get', 'route' => 'newautorouting', - 'route_params' => '', + 'route_params' => '[/..]', 'handler' => '\Tests\Support\Controllers\Newautorouting::getIndex', - 'params' => [], + 'params' => [ + 'm' => false, + ], ], [ 'method' => 'post', From 640094669a21c775169b44f45bb8a54eefb225c8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 27 Jan 2023 11:21:33 +0900 Subject: [PATCH 010/485] docs: add docs --- user_guide_src/source/changelogs/v4.4.0.rst | 5 +++- .../source/incoming/controllers.rst | 23 +++++++++++++++++++ .../source/incoming/controllers/024.php | 11 +++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/incoming/controllers/024.php diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 6632a061c4d1..748829333165 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -58,8 +58,11 @@ Helpers and Functions Others ====== -- **View:** Added optional 2nd parameter ``$saveData`` on ``renderSection()`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. +- **View:** Added optional 2nd parameter ``$saveData`` on ``renderSection()`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. +- **Auto Routing (Improved)**: Now you can use URI without a method name like + ``product/15`` where ``15`` is an arbitrary number. + See :ref:`controller-default-method-fallback` for details. Message Changes *************** diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index cd7023653b55..fda8d74cd1f5 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -263,6 +263,29 @@ Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): .. literalinclude:: controllers/022.php +.. _controller-default-method-fallback: + +Default Method Fallback +======================= + +.. versionadded:: 4.4.0 + +If the controller method corresponding to the URI segment of the method name +does not exist, and if the default method is defined, the URI segments are +passed to the default method for execution. + +.. literalinclude:: controllers/024.php + +Load the following URL:: + + example.com/index.php/product/15/edit + +The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): + +.. note:: If there are more parameters in the URI than the method parameters, + Auto Routing (Improved) does not execute the method, and it results in 404 + Not Found. + Defining a Default Controller ============================= diff --git a/user_guide_src/source/incoming/controllers/024.php b/user_guide_src/source/incoming/controllers/024.php new file mode 100644 index 000000000000..c3ae1a7c6a43 --- /dev/null +++ b/user_guide_src/source/incoming/controllers/024.php @@ -0,0 +1,11 @@ + Date: Mon, 16 Jan 2023 10:55:47 +0900 Subject: [PATCH 011/485] docs: add PHPDoc types --- app/Config/Filters.php | 6 ++++++ system/Filters/Filters.php | 10 ++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 7b70c4fb3381..68600771f5cd 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -14,6 +14,9 @@ class Filters extends BaseConfig /** * Configures aliases for Filter classes to * make reading things nicer and simpler. + * + * @var array + * @phpstan-var array */ public array $aliases = [ 'csrf' => CSRF::class, @@ -26,6 +29,9 @@ class Filters extends BaseConfig /** * List of filter aliases that are always * applied before and after every request. + * + * @var array>>|array> + * @phpstan-var array>|array>> */ public array $globals = [ 'before' => [ diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 30e36a32c65f..3c4468110531 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -63,7 +63,7 @@ class Filters * The processed filters that will * be used to check against. * - * @var array + * @var array */ protected $filters = [ 'before' => [], @@ -74,7 +74,7 @@ class Filters * The collection of filters' class names that will * be used to execute in each position. * - * @var array + * @var array */ protected $filtersClass = [ 'before' => [], @@ -84,14 +84,16 @@ class Filters /** * Any arguments to be passed to filters. * - * @var array + * @var array> [name => params] + * @phpstan-var array> */ protected $arguments = []; /** * Any arguments to be passed to filtersClass. * - * @var array + * @var array [classname => arguments] + * @phpstan-var array>|null> */ protected $argumentsClass = []; From 4aabafdbab14b78cbf2f41f860632802f72b26b1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 16 Jan 2023 11:02:06 +0900 Subject: [PATCH 012/485] style: break long lines --- system/Filters/Filters.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 3c4468110531..010476aec665 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -172,7 +172,10 @@ public function run(string $uri, string $position = 'before') } if ($position === 'before') { - $result = $class->before($this->request, $this->argumentsClass[$className] ?? null); + $result = $class->before( + $this->request, + $this->argumentsClass[$className] ?? null + ); if ($result instanceof RequestInterface) { $this->request = $result; @@ -195,7 +198,11 @@ public function run(string $uri, string $position = 'before') } if ($position === 'after') { - $result = $class->after($this->request, $this->response, $this->argumentsClass[$className] ?? null); + $result = $class->after( + $this->request, + $this->response, + $this->argumentsClass[$className] ?? null + ); if ($result instanceof ResponseInterface) { $this->response = $result; From c242ed5dc1993269f269c7ce19a44ba053639b23 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 21 Jan 2023 17:37:58 +0900 Subject: [PATCH 013/485] refactor: extract getCleanName() method --- system/Filters/Filters.php | 39 ++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 010476aec665..3875c075d8ad 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -325,22 +325,18 @@ public function addFilter(string $class, ?string $alias = null, string $when = ' * after the filter name, followed by a comma-separated list of arguments that * are passed to the filter when executed. * + * @param string $name filter_name or filter_name:arguments like 'role:admin,manager' + * * @return Filters * * @deprecated Use enableFilters(). This method will be private. */ public function enableFilter(string $name, string $when = 'before') { - // Get parameters and clean name - if (strpos($name, ':') !== false) { - [$name, $params] = explode(':', $name); - - $params = explode(',', $params); - array_walk($params, static function (&$item) { - $item = trim($item); - }); - - $this->arguments[$name] = $params; + // Get arguments and clean name + [$name, $arguments] = $this->getCleanName($name); + if ($arguments !== []) { + $this->arguments[$name] = $arguments; } if (class_exists($name)) { @@ -363,6 +359,29 @@ public function enableFilter(string $name, string $when = 'before') return $this; } + /** + * Get clean name and arguments + * + * @param string $name filter_name or filter_name:arguments like 'role:admin,manager' + * + * @return array [name, arguments] + */ + private function getCleanName(string $name): array + { + $arguments = []; + + if (strpos($name, ':') !== false) { + [$name, $arguments] = explode(':', $name); + + $arguments = explode(',', $arguments); + array_walk($arguments, static function (&$item) { + $item = trim($item); + }); + } + + return [$name, $arguments]; + } + /** * Ensures that specific filters are on and enabled for the current request. * From bd62d40a4746d5e3c206eb35a8d716844d2ab3c0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 21 Jan 2023 18:54:26 +0900 Subject: [PATCH 014/485] docs: add PHPDoc types --- system/Filters/Filters.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 3875c075d8ad..253e3ba9a792 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -365,6 +365,7 @@ public function enableFilter(string $name, string $when = 'before') * @param string $name filter_name or filter_name:arguments like 'role:admin,manager' * * @return array [name, arguments] + * @phpstan-return array{0: string, 1: list} */ private function getCleanName(string $name): array { @@ -389,6 +390,8 @@ private function getCleanName(string $name): array * after the filter name, followed by a comma-separated list of arguments that * are passed to the filter when executed. * + * @params array $names filter_name or filter_name:arguments like 'role:admin,manager' + * * @return Filters */ public function enableFilters(array $names, string $when = 'before') From ac730dddf43deed0275a00b4673e3b09f825501e Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 21 Jan 2023 18:55:08 +0900 Subject: [PATCH 015/485] docs: update comment --- system/Filters/Filters.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 253e3ba9a792..8dfd1a2082b4 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -531,8 +531,8 @@ protected function processAliasesToClass(string $position) } } - // when using enableFilter() we already write the class name in ->filtersClass as well as the - // alias in ->filters. This leads to duplicates when using route filters. + // when using enableFilter() we already write the class name in $filtersClass as well as the + // alias in $filters. This leads to duplicates when using route filters. // Since some filters like rate limiters rely on being executed once a request we filter em here. $this->filtersClass[$position] = array_values(array_unique($this->filtersClass[$position])); } From 2b923cd88aa3cbb4dfccc0d371b70683e131cea9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 21 Jan 2023 19:59:36 +0900 Subject: [PATCH 016/485] feat: $filters can use filter arguments --- system/Filters/Filters.php | 33 +++++++++++++++++++++++++-- tests/system/Filters/FiltersTest.php | 34 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 8dfd1a2082b4..2e198cb89049 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -494,20 +494,49 @@ protected function processFilters(?string $uri = null) // Look for inclusion rules if (isset($settings['before'])) { $path = $settings['before']; + if ($this->pathApplies($uri, $path)) { - $this->filters['before'][] = $alias; + // Get arguments and clean name + [$name, $arguments] = $this->getCleanName($alias); + + $this->filters['before'][] = $name; + + $this->registerArguments($name, $arguments); } } if (isset($settings['after'])) { $path = $settings['after']; + if ($this->pathApplies($uri, $path)) { - $this->filters['after'][] = $alias; + // Get arguments and clean name + [$name, $arguments] = $this->getCleanName($alias); + + $this->filters['after'][] = $name; + + $this->registerArguments($name, $arguments); } } } } + /** + * @param string $name filter alias + * @param array $arguments filter arguments + */ + private function registerArguments(string $name, $arguments): void + { + if ($arguments !== []) { + $this->arguments[$name] = $arguments; + } + + $classNames = (array) $this->config->aliases[$name]; + + foreach ($classNames as $className) { + $this->argumentsClass[$className] = $this->arguments[$name] ?? null; + } + } + /** * Maps filter aliases to the equivalent filter classes * diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index f111f4b957f6..93c425eff395 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -783,6 +783,40 @@ public function testEnableFilter() $this->assertContains('google', $filters['before']); } + public function testFiltersWithArguments() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['role' => Role::class], + 'globals' => [ + ], + 'filters' => [ + 'role:admin,super' => [ + 'before' => ['admin/*'], + 'after' => ['admin/*'], + ], + ], + ]; + $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config); + $filters = $this->createFilters($filtersConfig); + + $filters = $filters->initialize('admin/foo/bar'); + $found = $filters->getFilters(); + + $this->assertContains('role', $found['before']); + $this->assertSame(['admin', 'super'], $filters->getArguments('role')); + $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()); + } + public function testEnableFilterWithArguments() { $_SERVER['REQUEST_METHOD'] = 'GET'; From 29fdf817e5d15bb38a9746d1c8fac6835ae3e5c7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 21 Jan 2023 20:17:04 +0900 Subject: [PATCH 017/485] docs: add docs --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + user_guide_src/source/incoming/filters.rst | 25 ++++++++++++++++--- .../source/incoming/filters/009.php | 2 ++ .../source/incoming/filters/012.php | 17 +++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 user_guide_src/source/incoming/filters/012.php diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 748829333165..713322198bc4 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -63,6 +63,7 @@ Others - **Auto Routing (Improved)**: Now you can use URI without a method name like ``product/15`` where ``15`` is an arbitrary number. See :ref:`controller-default-method-fallback` for details. +- **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. Message Changes *************** diff --git a/user_guide_src/source/incoming/filters.rst b/user_guide_src/source/incoming/filters.rst index d768fa9879f7..944bb0c3e50d 100644 --- a/user_guide_src/source/incoming/filters.rst +++ b/user_guide_src/source/incoming/filters.rst @@ -139,15 +139,34 @@ a list of URI patterns that filter should apply to: .. literalinclude:: filters/009.php -Filter arguments -================= +Filter Arguments +================ -When configuring filters, additional arguments may be passed to a filter when setting up the route: +When configuring filters, additional arguments may be passed to a filter. + +Route +----- + +When setting up the route: .. literalinclude:: filters/010.php In this example, the array ``['dual', 'noreturn']`` will be passed in ``$arguments`` to the filter's ``before()`` and ``after()`` implementation methods. +.. _filter-arguments-filters: + +$filters +-------- + +When setting up the ``$filters``: + +.. literalinclude:: filters/012.php + +In this example, when the URI matches ``admin/*'``, the array ``['admin', 'superadmin']`` +will be passed in ``$arguments`` to the ``group`` filter's ``before()`` methods. +When the URI matches ``admin/users/*'``, the array ``['users.manage']`` +will be passed in ``$arguments`` to the ``permission`` filter's ``before()`` methods. + ****************** Confirming Filters ****************** diff --git a/user_guide_src/source/incoming/filters/009.php b/user_guide_src/source/incoming/filters/009.php index 162af6dcbaec..fd9119d21af5 100644 --- a/user_guide_src/source/incoming/filters/009.php +++ b/user_guide_src/source/incoming/filters/009.php @@ -6,6 +6,8 @@ class Filters extends BaseConfig { + // ... + public $filters = [ 'foo' => ['before' => ['admin/*'], 'after' => ['users/*']], 'bar' => ['before' => ['api/*', 'admin/*']], diff --git a/user_guide_src/source/incoming/filters/012.php b/user_guide_src/source/incoming/filters/012.php new file mode 100644 index 000000000000..799bd57fce7d --- /dev/null +++ b/user_guide_src/source/incoming/filters/012.php @@ -0,0 +1,17 @@ + ['before' => ['admin/*']], + 'permission:users.manage' => ['before' => ['admin/users/*']], + ]; + + // ... +} From a0827790c76d7ff4b38c395988d89c85d8842ca0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 21 Jan 2023 20:50:36 +0900 Subject: [PATCH 018/485] docs: improve docs --- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- user_guide_src/source/incoming/filters.rst | 22 +++++-------------- user_guide_src/source/incoming/routing.rst | 12 +++++++++- .../{filters/010.php => routing/067.php} | 0 4 files changed, 17 insertions(+), 19 deletions(-) rename user_guide_src/source/incoming/{filters/010.php => routing/067.php} (100%) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 713322198bc4..e3825c469530 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -63,7 +63,7 @@ Others - **Auto Routing (Improved)**: Now you can use URI without a method name like ``product/15`` where ``15`` is an arbitrary number. See :ref:`controller-default-method-fallback` for details. -- **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. +- **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. Message Changes *************** diff --git a/user_guide_src/source/incoming/filters.rst b/user_guide_src/source/incoming/filters.rst index 944bb0c3e50d..353661e510b6 100644 --- a/user_guide_src/source/incoming/filters.rst +++ b/user_guide_src/source/incoming/filters.rst @@ -139,26 +139,14 @@ a list of URI patterns that filter should apply to: .. literalinclude:: filters/009.php -Filter Arguments -================ - -When configuring filters, additional arguments may be passed to a filter. - -Route ------ - -When setting up the route: +.. _filters-filters-filter-arguments: -.. literalinclude:: filters/010.php - -In this example, the array ``['dual', 'noreturn']`` will be passed in ``$arguments`` to the filter's ``before()`` and ``after()`` implementation methods. - -.. _filter-arguments-filters: +Filter Arguments +---------------- -$filters --------- +.. versionadded:: 4.4.0 -When setting up the ``$filters``: +When configuring ``$filters``, additional arguments may be passed to a filter: .. literalinclude:: filters/012.php diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index fffa5cb1bdb8..f18edede419c 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -406,7 +406,7 @@ The value for the filter can be a string or an array of strings: * matching the aliases defined in **app/Config/Filters.php**. * filter classnames -See :doc:`Controller filters ` for more information on setting up filters. +See :doc:`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 (Legacy). @@ -446,6 +446,16 @@ You specify an array for the filter value: .. literalinclude:: routing/037.php +Filter Arguments +^^^^^^^^^^^^^^^^ + +Additional arguments may be passed to a filter: + +.. literalinclude:: routing/067.php + +In this example, the array ``['dual', 'noreturn']`` will be passed in ``$arguments`` +to the filter's ``before()`` and ``after()`` implementation methods. + .. _assigning-namespace: Assigning Namespace diff --git a/user_guide_src/source/incoming/filters/010.php b/user_guide_src/source/incoming/routing/067.php similarity index 100% rename from user_guide_src/source/incoming/filters/010.php rename to user_guide_src/source/incoming/routing/067.php From 9c36d01ff4b4120157485cd695e1d88548a886da Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 21 Jan 2023 21:34:38 +0900 Subject: [PATCH 019/485] docs: make @return more specific --- system/Filters/Filters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 2e198cb89049..fa17d505fb62 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -327,7 +327,7 @@ public function addFilter(string $class, ?string $alias = null, string $when = ' * * @param string $name filter_name or filter_name:arguments like 'role:admin,manager' * - * @return Filters + * @return $this * * @deprecated Use enableFilters(). This method will be private. */ From 4d525eb78a3ab47322a9cb33f700b78da1301376 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 21 Jan 2023 21:40:24 +0900 Subject: [PATCH 020/485] feat: throws exception if the filter arguments already defined --- system/Filters/Filters.php | 21 ++++++++--- tests/system/Filters/FiltersTest.php | 53 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index fa17d505fb62..829ad565cb1a 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Filters; +use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Filters\Exceptions\FilterException; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; @@ -84,8 +85,8 @@ class Filters /** * Any arguments to be passed to filters. * - * @var array> [name => params] - * @phpstan-var array> + * @var array|null> [name => params] + * @phpstan-var array|null> */ protected $arguments = []; @@ -337,6 +338,8 @@ public function enableFilter(string $name, string $when = 'before') [$name, $arguments] = $this->getCleanName($name); if ($arguments !== []) { $this->arguments[$name] = $arguments; + } else { + $this->arguments[$name] = null; } if (class_exists($name)) { @@ -514,7 +517,9 @@ protected function processFilters(?string $uri = null) $this->filters['after'][] = $name; - $this->registerArguments($name, $arguments); + // The arguments may have already been registered in the before filter. + // So disable check. + $this->registerArguments($name, $arguments, false); } } } @@ -523,10 +528,18 @@ protected function processFilters(?string $uri = null) /** * @param string $name filter alias * @param array $arguments filter arguments + * @param bool $check if true, check if already defined */ - private function registerArguments(string $name, $arguments): void + private function registerArguments(string $name, array $arguments, bool $check = true): void { if ($arguments !== []) { + if ($check && array_key_exists($name, $this->arguments)) { + throw new ConfigException( + '"' . $name . '" has already arguments: ' + . (($this->arguments[$name] === null) ? 'null' : implode(',', $this->arguments[$name])) + ); + } + $this->arguments[$name] = $arguments; } diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index 93c425eff395..9b01988e2e14 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Filters; use CodeIgniter\Config\Services; +use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Filters\Exceptions\FilterException; use CodeIgniter\Filters\fixtures\GoogleCurious; use CodeIgniter\Filters\fixtures\GoogleEmpty; @@ -817,6 +818,58 @@ public function testFiltersWithArguments() $this->assertSame('admin;super', $response->getBody()); } + public function testFilterWithArgumentsIsDefined() + { + $this->expectException(ConfigException::class); + $this->expectExceptionMessage('"role" has already arguments: admin,super'); + + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['role' => Role::class], + 'globals' => [], + 'filters' => [ + 'role:admin,super' => [ + 'before' => ['admin/*'], + ], + 'role:super' => [ + 'before' => ['admin/user/*'], + ], + ], + ]; + $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config); + $filters = $this->createFilters($filtersConfig); + + $filters->initialize('admin/user/bar'); + } + + public function testFilterWithoutArgumentsIsDefined() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['role' => Role::class], + 'globals' => [], + 'filters' => [ + 'role' => [ + 'before' => ['admin/*'], + ], + 'role:super' => [ + 'before' => ['admin/user/*'], + ], + ], + ]; + $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config); + $filters = $this->createFilters($filtersConfig); + + $filters = $filters->initialize('admin/user/bar'); + $found = $filters->getFilters(); + + $this->assertContains('role', $found['before']); + $this->assertSame(['super'], $filters->getArguments('role')); + $this->assertSame(['role' => ['super']], $filters->getArguments()); + } + public function testEnableFilterWithArguments() { $_SERVER['REQUEST_METHOD'] = 'GET'; From e3ea3ed468eae1cc6cbd599447a0621b931f899f Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 23 Jan 2023 11:48:51 +0900 Subject: [PATCH 021/485] refactor: by rector --- system/Filters/Filters.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 829ad565cb1a..2dc0efc66104 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -335,12 +335,8 @@ public function addFilter(string $class, ?string $alias = null, string $when = ' public function enableFilter(string $name, string $when = 'before') { // Get arguments and clean name - [$name, $arguments] = $this->getCleanName($name); - if ($arguments !== []) { - $this->arguments[$name] = $arguments; - } else { - $this->arguments[$name] = null; - } + [$name, $arguments] = $this->getCleanName($name); + $this->arguments[$name] = ($arguments !== []) ? $arguments : null; if (class_exists($name)) { $this->config->aliases[$name] = $name; From b768f8f8b80d70082b5fe716bc2db643abfb73ab Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 23 Jan 2023 11:59:04 +0900 Subject: [PATCH 022/485] docs: add about route filter config --- user_guide_src/source/incoming/filters.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/incoming/filters.rst b/user_guide_src/source/incoming/filters.rst index 353661e510b6..1a80df9d091b 100644 --- a/user_guide_src/source/incoming/filters.rst +++ b/user_guide_src/source/incoming/filters.rst @@ -6,10 +6,12 @@ Controller Filters :local: :depth: 2 -Controller Filters allow you to perform actions either before or after the controllers execute. Unlike :doc:`events `, -you can choose the specific URIs in which the filters will be applied to. Incoming filters may +Controller Filters allow you to perform actions either before or after the controllers execute. Unlike :doc:`/extending/events`, +you can choose the specific URIs or routes in which the filters will be applied to. Before filters may modify the Request while after filters can act on and even modify the Response, allowing for a lot of flexibility -and power. Some common examples of tasks that might be performed with filters are: +and power. + +Some common examples of tasks that might be performed with filters are: * Performing CSRF protection on the incoming requests * Restricting areas of your site based upon their Role @@ -64,11 +66,13 @@ 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**. -This file contains four properties that allow you to configure exactly when the filters run. +Once you've created your filters, you need to configure when they get run. This is done in **app/Config/Filters.php** or **app/Config/Routes.php**. .. Note:: The safest way to apply filters is to :ref:`disable auto-routing `, and :ref:`set filters to routes `. +The **app/Config/Filters.php** file contains four properties that allow you to +configure exactly when the filters run. + .. Warning:: It is recommended that you should always add ``*`` at the end of a URI in the filter settings. Because a controller method might be accessible by different URLs than you think. For example, when :ref:`auto-routing-legacy` is enabled, if you have ``Blog::index``, From 5c46faaec0ae034e92cc298107a3018fd1c49052 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Jan 2023 16:57:43 +0900 Subject: [PATCH 023/485] fix: exception message Co-authored-by: MGatner --- system/Filters/Filters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 2dc0efc66104..4ef34431723f 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -531,7 +531,7 @@ private function registerArguments(string $name, array $arguments, bool $check = if ($arguments !== []) { if ($check && array_key_exists($name, $this->arguments)) { throw new ConfigException( - '"' . $name . '" has already arguments: ' + '"' . $name . '" already has arguments: ' . (($this->arguments[$name] === null) ? 'null' : implode(',', $this->arguments[$name])) ); } From 66083fa32491665a2a5de973b08e30e6b428788e Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Jan 2023 17:06:45 +0900 Subject: [PATCH 024/485] test: update test --- 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 9b01988e2e14..88924de7bcff 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -821,7 +821,7 @@ public function testFiltersWithArguments() public function testFilterWithArgumentsIsDefined() { $this->expectException(ConfigException::class); - $this->expectExceptionMessage('"role" has already arguments: admin,super'); + $this->expectExceptionMessage('"role" already has arguments: admin,super'); $_SERVER['REQUEST_METHOD'] = 'GET'; From 9d664901fff60064ba62ab530a17373d7715fd9f Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Jan 2023 17:28:47 +0900 Subject: [PATCH 025/485] test: add test --- tests/system/CodeIgniterTest.php | 41 ++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 1a5e50fc0775..16c7acd1a5e3 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter; use CodeIgniter\Config\Services; +use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\HTTP\Response; use CodeIgniter\Router\RouteCollection; use CodeIgniter\Test\CIUnitTestCase; @@ -19,7 +20,7 @@ use CodeIgniter\Test\Mock\MockCodeIgniter; use Config\App; use Config\Cache; -use Config\Filters; +use Config\Filters as FiltersConfig; use Config\Modules; use Tests\Support\Filters\Customfilter; @@ -274,6 +275,42 @@ public function testControllersRunFilterByClassName() $this->resetServices(); } + public function testRegisterSameFilterTwiceWithDifferentArgument() + { + $this->expectException(ConfigException::class); + $this->expectExceptionMessage('"test-customfilter" already has arguments: null'); + + $_SERVER['argv'] = ['index.php', 'pages/about']; + $_SERVER['argc'] = 2; + + $_SERVER['REQUEST_URI'] = '/pages/about'; + + $routes = Services::routes(); + $routes->add( + 'pages/about', + static fn () => Services::incomingrequest()->getBody(), + // Set filter with no argument. + ['filter' => 'test-customfilter'] + ); + + $router = Services::router($routes, Services::incomingrequest()); + Services::injectMock('router', $router); + + /** @var FiltersConfig $filterConfig */ + $filterConfig = config('Filters'); + $filterConfig->filters = [ + // Set filter with argument. + 'test-customfilter:arg1' => [ + 'before' => ['pages/*'], + ], + ]; + Services::filters($filterConfig); + + $this->codeigniter->run(); + + $this->resetServices(); + } + public function testDisableControllerFilters() { $_SERVER['argv'] = ['index.php', 'pages/about']; @@ -666,7 +703,7 @@ public function testPageCacheSendSecureHeaders() $router = Services::router($routes, Services::incomingrequest()); Services::injectMock('router', $router); - /** @var Filters $filterConfig */ + /** @var FiltersConfig $filterConfig */ $filterConfig = config('Filters'); $filterConfig->globals['after'] = ['secureheaders']; Services::filters($filterConfig); From 472562d984f1c8940c18a22876f5510613f0fe3d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Jan 2023 17:31:11 +0900 Subject: [PATCH 026/485] test: improve test method --- tests/system/CodeIgniterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 16c7acd1a5e3..fa9b8e0f71d2 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -252,7 +252,7 @@ public function testControllersCanReturnDownloadResponseObject() $this->assertSame('some text', $output); } - public function testControllersRunFilterByClassName() + public function testRunExecuteFilterByClassName() { $_SERVER['argv'] = ['index.php', 'pages/about']; $_SERVER['argc'] = 2; From 46727ea30b4e14fcddcd0caa5c298f441f0104e2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 Jan 2023 17:43:32 +0900 Subject: [PATCH 027/485] style: break long lines --- tests/system/CodeIgniterTest.php | 62 ++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index fa9b8e0f71d2..daea86b8cc51 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -189,7 +189,10 @@ public function testControllersCanReturnString() // Inject mock router. $routes = Services::routes(); - $routes->add('pages/(:segment)', static fn ($segment) => 'You want to see "' . esc($segment) . '" page.'); + $routes->add( + 'pages/(:segment)', + static fn ($segment) => 'You want to see "' . esc($segment) . '" page.' + ); $router = Services::router($routes, Services::incomingrequest()); Services::injectMock('router', $router); @@ -261,7 +264,11 @@ public function testRunExecuteFilterByClassName() // Inject mock router. $routes = Services::routes(); - $routes->add('pages/about', static fn () => Services::incomingrequest()->getBody(), ['filter' => Customfilter::class]); + $routes->add( + 'pages/about', + static fn () => Services::incomingrequest()->getBody(), + ['filter' => Customfilter::class] + ); $router = Services::router($routes, Services::incomingrequest()); Services::injectMock('router', $router); @@ -743,8 +750,11 @@ public function testPageCacheSendSecureHeaders() * * @see https://github.com/codeigniter4/CodeIgniter4/pull/6410 */ - public function testPageCacheWithCacheQueryString($cacheQueryStringValue, int $expectedPagesInCache, array $testingUrls) - { + public function testPageCacheWithCacheQueryString( + $cacheQueryStringValue, + int $expectedPagesInCache, + array $testingUrls + ) { // Suppress command() output CITestStreamFilter::$buffer = ''; $outputStreamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); @@ -766,7 +776,10 @@ public function testPageCacheWithCacheQueryString($cacheQueryStringValue, int $e $_SERVER['REQUEST_URI'] = '/' . $testingUrl; $routes = Services::routes(true); $routes->add($testingUrl, static function () { - CodeIgniter::cache(0); // Don't cache the page in the run() function because CodeIgniter class will create default $cacheConfig and overwrite settings from the dataProvider + // Don't cache the page in the run() function because CodeIgniter + // class will create default $cacheConfig and overwrite settings + // from the dataProvider + CodeIgniter::cache(0); $response = Services::response(); $string = 'This is a test page, to check cache configuration'; @@ -777,9 +790,11 @@ public function testPageCacheWithCacheQueryString($cacheQueryStringValue, int $e $router = Services::router($routes, Services::incomingrequest(null, false)); Services::injectMock('router', $router); - // Cache the page output using default caching function and $cacheConfig with value from the data provider + // Cache the page output using default caching function and $cacheConfig + // with value from the data provider $this->codeigniter->run(); - $this->codeigniter->cachePage($cacheConfig); // Cache the page using our own $cacheConfig confugration + // Cache the page using our own $cacheConfig confugration + $this->codeigniter->cachePage($cacheConfig); } // Calculate how much cached items exist in the cache after the test requests @@ -800,17 +815,34 @@ public function testPageCacheWithCacheQueryString($cacheQueryStringValue, int $e public function cacheQueryStringProvider(): array { $testingUrls = [ - 'test', // URL #1 - 'test?important_parameter=1', // URL #2 - 'test?important_parameter=2', // URL #3 - 'test?important_parameter=1¬_important_parameter=2', // URL #4 - 'test?important_parameter=1¬_important_parameter=2&another_not_important_parameter=3', // URL #5 + // URL #1 + 'test', + // URL #2 + 'test?important_parameter=1', + // URL #3 + 'test?important_parameter=2', + // URL #4 + 'test?important_parameter=1¬_important_parameter=2', + // URL #5 + 'test?important_parameter=1¬_important_parameter=2&another_not_important_parameter=3', ]; return [ - '$cacheQueryString=false' => [false, 1, $testingUrls], // We expect only 1 page in the cache, because when cacheQueryString is set to false, all GET parameter should be ignored, and page URI will be absolutely same "/test" string for all 5 requests - '$cacheQueryString=true' => [true, 5, $testingUrls], // We expect all 5 pages in the cache, because when cacheQueryString is set to true, all GET parameter should be processed as unique requests - '$cacheQueryString=array' => [['important_parameter'], 3, $testingUrls], // We expect only 3 pages in the cache, because when cacheQueryString is set to array with important parameters, we should ignore all parameters thats not in the array. Only URL #1, URL #2 and URL #3 should be cached. URL #4 and URL #5 is duplication of URL #2 (with value ?important_parameter=1), so they should not be processed as new unique requests and application should return already cached page for URL #2 + // We expect only 1 page in the cache, because when cacheQueryString + // is set to false, all GET parameter should be ignored, and page URI + // will be absolutely same "/test" string for all 5 requests + '$cacheQueryString=false' => [false, 1, $testingUrls], + // We expect all 5 pages in the cache, because when cacheQueryString + // is set to true, all GET parameter should be processed as unique requests + '$cacheQueryString=true' => [true, 5, $testingUrls], + // We expect only 3 pages in the cache, because when cacheQueryString + // is set to array with important parameters, we should ignore all + // parameters thats not in the array. Only URL #1, URL #2 and URL #3 + // should be cached. URL #4 and URL #5 is duplication of URL #2 + // (with value ?important_parameter=1), so they should not be processed + // as new unique requests and application should return already cached + // page for URL #2 + '$cacheQueryString=array' => [['important_parameter'], 3, $testingUrls], ]; } } From 89f499e54cd952dde81b669c439ae4fb534325b0 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Wed, 1 Feb 2023 09:04:08 +0800 Subject: [PATCH 028/485] Feature: New method DownloadResponse::inline() --- system/HTTP/DownloadResponse.php | 17 ++++++++++++++++- tests/system/HTTP/DownloadResponseTest.php | 8 ++++++++ user_guide_src/source/changelogs/v4.4.0.rst | 4 ++++ user_guide_src/source/outgoing/response.rst | 8 ++++++++ user_guide_src/source/outgoing/response/028.php | 6 ++++++ 5 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/outgoing/response/028.php diff --git a/system/HTTP/DownloadResponse.php b/system/HTTP/DownloadResponse.php index 899bec8583c6..022f12560f5c 100644 --- a/system/HTTP/DownloadResponse.php +++ b/system/HTTP/DownloadResponse.php @@ -269,7 +269,10 @@ public function buildHeaders() $this->setContentTypeByMimeType(); } - $this->setHeader('Content-Disposition', $this->getContentDisposition()); + if (! $this->hasHeader('Content-Disposition')) { + $this->setHeader('Content-Disposition', $this->getContentDisposition()); + } + $this->setHeader('Expires-Disposition', '0'); $this->setHeader('Content-Transfer-Encoding', 'binary'); $this->setHeader('Content-Length', (string) $this->getContentLength()); @@ -325,4 +328,16 @@ private function sendBodyByBinary() return $this; } + + /** + * Sets the response header to display the file in the browser. + * + * @return DownloadResponse + */ + public function inline() + { + $this->setHeader('Content-Disposition', 'inline'); + + return $this; + } } diff --git a/tests/system/HTTP/DownloadResponseTest.php b/tests/system/HTTP/DownloadResponseTest.php index fe320705b290..7068434a6fda 100644 --- a/tests/system/HTTP/DownloadResponseTest.php +++ b/tests/system/HTTP/DownloadResponseTest.php @@ -120,6 +120,14 @@ public function testSetFileName() $this->assertSame('attachment; filename="myFile.txt"; filename*=UTF-8\'\'myFile.txt', $response->getHeaderLine('Content-Disposition')); } + public function testDispositionInline(): void + { + $response = new DownloadResponse('unit-test.txt', true); + $response->inline(); + $response->buildHeaders(); + $this->assertSame('inline', $response->getHeaderLine('Content-Disposition')); + } + public function testNoCache() { $response = new DownloadResponse('unit-test.txt', true); diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 748829333165..1f1705dbd877 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -28,6 +28,8 @@ Method Signature Changes Enhancements ************ +- Added ``DownloadResponse::inline()`` method that sets the ``Content-Disposition: inline`` header to display the file +in the browser. Commands ======== @@ -69,6 +71,8 @@ Message Changes Changes ******* +- The ``DownloadResponse`` class, when generating response headers, does not replace the ``Content-Disposition`` header +if it was previously specified. Deprecations ************ diff --git a/user_guide_src/source/outgoing/response.rst b/user_guide_src/source/outgoing/response.rst index c3c10bc8d40d..e4fabdf8d451 100644 --- a/user_guide_src/source/outgoing/response.rst +++ b/user_guide_src/source/outgoing/response.rst @@ -93,6 +93,14 @@ Use the optional ``setFileName()`` method to change the filename as it is sent t .. note:: The response object MUST be returned for the download to be sent to the client. This allows the response to be passed through all **after** filters before being sent to the client. +Open file in browser +-------------------- + +Some browsers can display files such as PDF. To tell the browser to display the file instead of saving it, call the +``DownloadResponse::inline()`` method. + +.. literalinclude:: response/007.php + HTTP Caching ============ diff --git a/user_guide_src/source/outgoing/response/028.php b/user_guide_src/source/outgoing/response/028.php new file mode 100644 index 000000000000..8afaf84960ab --- /dev/null +++ b/user_guide_src/source/outgoing/response/028.php @@ -0,0 +1,6 @@ +response->download($name, $data)->inline(); From 2f5980922257da10db9843db25d25986223d83bb Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Wed, 1 Feb 2023 09:22:07 +0800 Subject: [PATCH 029/485] fix doc --- user_guide_src/source/changelogs/v4.4.0.rst | 6 ++++-- user_guide_src/source/outgoing/response.rst | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 1f1705dbd877..bfcd77504631 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -28,8 +28,9 @@ Method Signature Changes Enhancements ************ -- Added ``DownloadResponse::inline()`` method that sets the ``Content-Disposition: inline`` header to display the file -in the browser. + +- Added ``DownloadResponse::inline()`` method that sets the ``Content-Disposition: inline`` header to display the +file in the browser. Commands ======== @@ -71,6 +72,7 @@ Message Changes Changes ******* + - The ``DownloadResponse`` class, when generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. diff --git a/user_guide_src/source/outgoing/response.rst b/user_guide_src/source/outgoing/response.rst index e4fabdf8d451..914c0f94734d 100644 --- a/user_guide_src/source/outgoing/response.rst +++ b/user_guide_src/source/outgoing/response.rst @@ -99,7 +99,7 @@ Open file in browser Some browsers can display files such as PDF. To tell the browser to display the file instead of saving it, call the ``DownloadResponse::inline()`` method. -.. literalinclude:: response/007.php +.. literalinclude:: response/028.php HTTP Caching ============ From c8795464108db008466e8cbc453caef032c5cf7d Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Wed, 1 Feb 2023 09:37:25 +0800 Subject: [PATCH 030/485] fix doc2 --- user_guide_src/source/changelogs/v4.4.0.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index bfcd77504631..8d46a98b9cff 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -29,8 +29,7 @@ Method Signature Changes Enhancements ************ -- Added ``DownloadResponse::inline()`` method that sets the ``Content-Disposition: inline`` header to display the -file in the browser. +- Added ``DownloadResponse::inline()`` method that sets the ``Content-Disposition: inline`` header to display the file in the browser. Commands ======== @@ -73,8 +72,7 @@ Message Changes Changes ******* -- The ``DownloadResponse`` class, when generating response headers, does not replace the ``Content-Disposition`` header -if it was previously specified. +- The ``DownloadResponse`` class, when generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. Deprecations ************ From 9ccc497cb73f70eb7f7c80f3a660b177699fa051 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 1 Feb 2023 22:29:17 +0900 Subject: [PATCH 031/485] feat: add --host option to `spark routes` --- system/Commands/Utilities/Routes.php | 24 ++++++++++- tests/_support/Config/Routes.php | 3 ++ tests/system/Commands/RoutesTest.php | 61 +++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index bcd5825c649e..3b0112db828a 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -69,7 +69,8 @@ class Routes extends BaseCommand * @var array */ protected $options = [ - '-h' => 'Sort by Handler.', + '-h' => 'Sort by Handler.', + '--host' => 'Specify hostname in request URI.', ]; /** @@ -78,9 +79,24 @@ class Routes extends BaseCommand public function run(array $params) { $sortByHandler = array_key_exists('h', $params); + $host = $params['host'] ?? null; + + // Set HTTP_HOST + if ($host) { + $request = Services::request(); + $_SERVER = $request->getServer(); + $_SERVER['HTTP_HOST'] = $host; + $request->setGlobal('server', $_SERVER); + } $collection = Services::routes()->loadRoutes(); - $methods = [ + + // Reset HTTP_HOST + if ($host) { + unset($_SERVER['HTTP_HOST']); + } + + $methods = [ 'get', 'head', 'post', @@ -171,6 +187,10 @@ public function run(array $params) usort($tbody, static fn ($handler1, $handler2) => strcmp($handler1[3], $handler2[3])); } + if ($host) { + CLI::write('Host: ' . $host); + } + CLI::table($tbody, $thead); } } diff --git a/tests/_support/Config/Routes.php b/tests/_support/Config/Routes.php index ea288729aa9d..4ba439d4d730 100644 --- a/tests/_support/Config/Routes.php +++ b/tests/_support/Config/Routes.php @@ -14,3 +14,6 @@ // This is a simple file to include for testing the RouteCollection class. $routes->add('testing', 'TestController::index', ['as' => 'testing-index']); $routes->get('closure', static fn () => 'closure test'); +$routes->get('/', 'Blog::index', ['hostname' => 'blog.example.com']); +$routes->get('/', 'Sub::index', ['subdomain' => 'sub']); +$routes->get('/all', 'AllDomain::index', ['subdomain' => '*']); diff --git a/tests/system/Commands/RoutesTest.php b/tests/system/Commands/RoutesTest.php index 8567dc6bc70a..c183686e6d05 100644 --- a/tests/system/Commands/RoutesTest.php +++ b/tests/system/Commands/RoutesTest.php @@ -53,7 +53,7 @@ private function getCleanRoutes(): RouteCollection public function testRoutesCommand() { - $this->getCleanRoutes(); + Services::injectMock('routes', null); command('routes'); @@ -79,7 +79,7 @@ public function testRoutesCommand() public function testRoutesCommandSortByHandler() { - $this->getCleanRoutes(); + Services::injectMock('routes', null); command('routes -h'); @@ -103,6 +103,62 @@ public function testRoutesCommandSortByHandler() $this->assertStringContainsString($expected, $this->getBuffer()); } + public function testRoutesCommandHostHostname() + { + Services::injectMock('routes', null); + + command('routes --host blog.example.com'); + + $expected = <<<'EOL' + Host: blog.example.com + +---------+---------+---------------+----------------------------------------+----------------+---------------+ + | Method | Route | Name | Handler | Before Filters | After Filters | + +---------+---------+---------------+----------------------------------------+----------------+---------------+ + | GET | / | » | \App\Controllers\Blog::index | | toolbar | + | GET | closure | » | (Closure) | | toolbar | + | GET | all | » | \App\Controllers\AllDomain::index | | toolbar | + | GET | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | HEAD | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | POST | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | PUT | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | DELETE | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | OPTIONS | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | TRACE | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | CONNECT | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | CLI | testing | testing-index | \App\Controllers\TestController::index | | | + +---------+---------+---------------+----------------------------------------+----------------+---------------+ + EOL; + $this->assertStringContainsString($expected, $this->getBuffer()); + } + + public function testRoutesCommandHostSubdomain() + { + Services::injectMock('routes', null); + + command('routes --host sub.example.com'); + + $expected = <<<'EOL' + Host: sub.example.com + +---------+---------+---------------+----------------------------------------+----------------+---------------+ + | Method | Route | Name | Handler | Before Filters | After Filters | + +---------+---------+---------------+----------------------------------------+----------------+---------------+ + | GET | / | » | \App\Controllers\Sub::index | | toolbar | + | GET | closure | » | (Closure) | | toolbar | + | GET | all | » | \App\Controllers\AllDomain::index | | toolbar | + | GET | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | HEAD | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | POST | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | PUT | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | DELETE | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | OPTIONS | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | TRACE | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | CONNECT | testing | testing-index | \App\Controllers\TestController::index | | toolbar | + | CLI | testing | testing-index | \App\Controllers\TestController::index | | | + +---------+---------+---------------+----------------------------------------+----------------+---------------+ + EOL; + $this->assertStringContainsString($expected, $this->getBuffer()); + } + public function testRoutesCommandAutoRouteImproved() { $routes = $this->getCleanRoutes(); @@ -139,6 +195,7 @@ public function testRoutesCommandAutoRouteImproved() public function testRoutesCommandRouteLegacy() { $routes = $this->getCleanRoutes(); + $routes->loadRoutes(); $routes->setAutoRoute(true); $namespace = 'Tests\Support\Controllers'; From 78280475dd71bfb1bfcbac3a01ce35302633eb0c Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Feb 2023 14:09:46 +0900 Subject: [PATCH 032/485] docs: add docs --- user_guide_src/source/changelogs/v4.4.0.rst | 3 +++ user_guide_src/source/incoming/routing.rst | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 748829333165..ea3c5f90c326 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -32,6 +32,9 @@ Enhancements Commands ======== +- Now ``spark routes`` command can specify the host in the request URL. + See :ref:`routing-spark-routes-specify-host`. + Testing ======= diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index fffa5cb1bdb8..5c1303b99df2 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -858,3 +858,14 @@ Sort by Handler You can sort the routes by *Handler*:: > php spark routes -h + +.. _routing-spark-routes-specify-host: + +Specify Host +------------ + +.. versionadded:: 4.4.0 + +You can specify the host in the request URL with the ``--host`` option:: + + > php spark routes --host accounts.example.com From 15c3528cbe16a087c0b09860ed57f49c8f59b047 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Feb 2023 14:22:45 +0900 Subject: [PATCH 033/485] chore: update psalm-baseline.xml --- psalm-baseline.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 6fe1103175b9..fa3a6f719714 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -125,8 +125,7 @@ - - $routes + $routes From 6469b0704597b4c2883469fa9d94b369ec929876 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Sat, 4 Feb 2023 07:52:20 +0800 Subject: [PATCH 034/485] Documentation and changelog changes --- user_guide_src/source/changelogs/v4.4.0.rst | 7 ++++--- user_guide_src/source/outgoing/response.rst | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 8d46a98b9cff..59321b3d0b54 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -29,8 +29,6 @@ Method Signature Changes Enhancements ************ -- Added ``DownloadResponse::inline()`` method that sets the ``Content-Disposition: inline`` header to display the file in the browser. - Commands ======== @@ -61,6 +59,9 @@ Helpers and Functions Others ====== +- **DownloadResponse:** Added ``DownloadResponse::inline()`` method that sets + the ``Content-Disposition: inline`` header to display the file in the browser. + See :ref:`open-file-in-browser` for details. - **View:** Added optional 2nd parameter ``$saveData`` on ``renderSection()`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. - **Auto Routing (Improved)**: Now you can use URI without a method name like ``product/15`` where ``15`` is an arbitrary number. @@ -72,7 +73,7 @@ Message Changes Changes ******* -- The ``DownloadResponse`` class, when generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. +- **DownloadResponse:** When generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. Deprecations ************ diff --git a/user_guide_src/source/outgoing/response.rst b/user_guide_src/source/outgoing/response.rst index 914c0f94734d..3f435702c34d 100644 --- a/user_guide_src/source/outgoing/response.rst +++ b/user_guide_src/source/outgoing/response.rst @@ -93,7 +93,9 @@ Use the optional ``setFileName()`` method to change the filename as it is sent t .. note:: The response object MUST be returned for the download to be sent to the client. This allows the response to be passed through all **after** filters before being sent to the client. -Open file in browser +.. _open-file-in-browser: + +Open File in Browser -------------------- Some browsers can display files such as PDF. To tell the browser to display the file instead of saving it, call the From 967f1d80e2d4685679039dd412a9acb8ca1b06df Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 23 Dec 2022 13:51:39 +0900 Subject: [PATCH 035/485] refactor: remove Cookie config items in Config\App --- app/Config/App.php | 79 ------------------------- system/HTTP/Response.php | 26 +------- system/Helpers/cookie_helper.php | 6 +- system/Security/Security.php | 14 ++--- system/Session/Handlers/BaseHandler.php | 19 ++---- system/Session/Session.php | 14 ++--- system/Test/Mock/MockAppConfig.php | 6 -- system/Test/Mock/MockCLIConfig.php | 6 -- 8 files changed, 16 insertions(+), 154 deletions(-) diff --git a/app/Config/App.php b/app/Config/App.php index f598e324152a..a5774a0e25b5 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -238,85 +238,6 @@ class App extends BaseConfig */ public ?string $sessionDBGroup = null; - /** - * -------------------------------------------------------------------------- - * Cookie Prefix - * -------------------------------------------------------------------------- - * - * Set a cookie name prefix if you need to avoid collisions. - * - * @deprecated use Config\Cookie::$prefix property instead. - */ - public string $cookiePrefix = ''; - - /** - * -------------------------------------------------------------------------- - * Cookie Domain - * -------------------------------------------------------------------------- - * - * Set to `.your-domain.com` for site-wide cookies. - * - * @deprecated use Config\Cookie::$domain property instead. - */ - public string $cookieDomain = ''; - - /** - * -------------------------------------------------------------------------- - * Cookie Path - * -------------------------------------------------------------------------- - * - * Typically will be a forward slash. - * - * @deprecated use Config\Cookie::$path property instead. - */ - public string $cookiePath = '/'; - - /** - * -------------------------------------------------------------------------- - * Cookie Secure - * -------------------------------------------------------------------------- - * - * Cookie will only be set if a secure HTTPS connection exists. - * - * @deprecated use Config\Cookie::$secure property instead. - */ - public bool $cookieSecure = false; - - /** - * -------------------------------------------------------------------------- - * Cookie HttpOnly - * -------------------------------------------------------------------------- - * - * Cookie will only be accessible via HTTP(S) (no JavaScript). - * - * @deprecated use Config\Cookie::$httponly property instead. - */ - public bool $cookieHTTPOnly = true; - - /** - * -------------------------------------------------------------------------- - * Cookie SameSite - * -------------------------------------------------------------------------- - * - * Configure cookie SameSite setting. Allowed values are: - * - None - * - Lax - * - Strict - * - '' - * - * Alternatively, you can use the constant names: - * - `Cookie::SAMESITE_NONE` - * - `Cookie::SAMESITE_LAX` - * - `Cookie::SAMESITE_STRICT` - * - * Defaults to `Lax` for compatibility with modern browsers. Setting `''` - * (empty string) means default SameSite attribute set by browsers (`Lax`) - * will be set on cookies. If set to `None`, `$cookieSecure` must also be set. - * - * @deprecated use Config\Cookie::$samesite property instead. - */ - public ?string $cookieSameSite = 'Lax'; - /** * -------------------------------------------------------------------------- * Reverse Proxy IPs diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index 21db9acd4191..d1b61e98562f 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -13,7 +13,6 @@ use CodeIgniter\Cookie\Cookie; use CodeIgniter\Cookie\CookieStore; -use CodeIgniter\Cookie\Exceptions\CookieException; use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\App; use Config\Services; @@ -156,31 +155,8 @@ public function __construct($config) $this->CSPEnabled = $config->CSPEnabled; - // DEPRECATED COOKIE MANAGEMENT - - $this->cookiePrefix = $config->cookiePrefix; - $this->cookieDomain = $config->cookieDomain; - $this->cookiePath = $config->cookiePath; - $this->cookieSecure = $config->cookieSecure; - $this->cookieHTTPOnly = $config->cookieHTTPOnly; - $this->cookieSameSite = $config->cookieSameSite ?? Cookie::SAMESITE_LAX; - - $config->cookieSameSite ??= Cookie::SAMESITE_LAX; - - if (! in_array(strtolower($config->cookieSameSite ?: Cookie::SAMESITE_LAX), Cookie::ALLOWED_SAMESITE_VALUES, true)) { - throw CookieException::forInvalidSameSite($config->cookieSameSite); - } - $this->cookieStore = new CookieStore([]); - Cookie::setDefaults(config('Cookie') ?? [ - // @todo Remove this fallback when deprecated `App` members are removed - 'prefix' => $config->cookiePrefix, - 'path' => $config->cookiePath, - 'domain' => $config->cookieDomain, - 'secure' => $config->cookieSecure, - 'httponly' => $config->cookieHTTPOnly, - 'samesite' => $config->cookieSameSite ?? Cookie::SAMESITE_LAX, - ]); + Cookie::setDefaults(config('Cookie')); // Default to an HTML Content-Type. Devs can override if needed. $this->setContentType('text/html'); diff --git a/system/Helpers/cookie_helper.php b/system/Helpers/cookie_helper.php index 0a11ce84b76d..592863739eba 100755 --- a/system/Helpers/cookie_helper.php +++ b/system/Helpers/cookie_helper.php @@ -9,7 +9,6 @@ * the LICENSE file that was distributed with this source code. */ -use Config\App; use Config\Cookie; use Config\Services; @@ -68,11 +67,10 @@ function set_cookie( function get_cookie($index, bool $xssClean = false, ?string $prefix = '') { if ($prefix === '') { - /** @var Cookie|null $cookie */ + /** @var Cookie $cookie */ $cookie = config('Cookie'); - // @TODO Remove Config\App fallback when deprecated `App` members are removed. - $prefix = $cookie instanceof Cookie ? $cookie->prefix : config('App')->cookiePrefix; + $prefix = $cookie->prefix; } $request = Services::request(); diff --git a/system/Security/Security.php b/system/Security/Security.php index 422e589b81cd..210e9a71a72d 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -222,18 +222,12 @@ private function configureSession(): void private function configureCookie(App $config): void { - /** @var CookieConfig|null $cookie */ + /** @var CookieConfig $cookie */ $cookie = config('Cookie'); - 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; - } + $cookiePrefix = $cookie->prefix; + $this->cookieName = $cookiePrefix . $this->rawCookieName; + Cookie::setDefaults($cookie); } /** diff --git a/system/Session/Handlers/BaseHandler.php b/system/Session/Handlers/BaseHandler.php index f7d6ff1a90c7..a579eb7e2f03 100644 --- a/system/Session/Handlers/BaseHandler.php +++ b/system/Session/Handlers/BaseHandler.php @@ -122,22 +122,13 @@ public function __construct(AppConfig $config, string $ipAddress) $this->savePath = $config->sessionSavePath; } - /** @var CookieConfig|null $cookie */ + /** @var CookieConfig $cookie */ $cookie = config('Cookie'); - if ($cookie instanceof CookieConfig) { - // Session cookies have no prefix. - $this->cookieDomain = $cookie->domain; - $this->cookiePath = $cookie->path; - $this->cookieSecure = $cookie->secure; - } else { - // @TODO Remove this fallback when deprecated `App` members are removed. - // `Config/Cookie.php` is absence - // Session cookies have no prefix. - $this->cookieDomain = $config->cookieDomain; - $this->cookiePath = $config->cookiePath; - $this->cookieSecure = $config->cookieSecure; - } + // Session cookies have no prefix. + $this->cookieDomain = $cookie->domain; + $this->cookiePath = $cookie->path; + $this->cookieSecure = $cookie->secure; $this->ipAddress = $ipAddress; } diff --git a/system/Session/Session.php b/system/Session/Session.php index 497111b60030..3ea5bcd00b53 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -188,22 +188,16 @@ public function __construct(SessionHandlerInterface $driver, App $config) $this->sessionRegenerateDestroy = $config->sessionRegenerateDestroy ?? $this->sessionRegenerateDestroy; } - // DEPRECATED COOKIE MANAGEMENT - $this->cookiePath = $config->cookiePath ?? $this->cookiePath; - $this->cookieDomain = $config->cookieDomain ?? $this->cookieDomain; - $this->cookieSecure = $config->cookieSecure ?? $this->cookieSecure; - $this->cookieSameSite = $config->cookieSameSite ?? $this->cookieSameSite; - /** @var CookieConfig $cookie */ $cookie = config('Cookie'); $this->cookie = (new Cookie($this->sessionCookieName, '', [ 'expires' => $this->sessionExpiration === 0 ? 0 : Time::now()->getTimestamp() + $this->sessionExpiration, - 'path' => $cookie->path ?? $config->cookiePath, - 'domain' => $cookie->domain ?? $config->cookieDomain, - 'secure' => $cookie->secure ?? $config->cookieSecure, + 'path' => $cookie->path, + 'domain' => $cookie->domain, + 'secure' => $cookie->secure, 'httponly' => true, // for security - 'samesite' => $cookie->samesite ?? $config->cookieSameSite ?? Cookie::SAMESITE_LAX, + 'samesite' => $cookie->samesite ?? Cookie::SAMESITE_LAX, 'raw' => $cookie->raw ?? false, ]))->withPrefix(''); // Cookie prefix should be ignored. diff --git a/system/Test/Mock/MockAppConfig.php b/system/Test/Mock/MockAppConfig.php index e21f7a6b7adf..0d3a22cc3ff6 100644 --- a/system/Test/Mock/MockAppConfig.php +++ b/system/Test/Mock/MockAppConfig.php @@ -17,12 +17,6 @@ class MockAppConfig extends App { public string $baseURL = 'http://example.com/'; public string $uriProtocol = 'REQUEST_URI'; - public string $cookiePrefix = ''; - public string $cookieDomain = ''; - public string $cookiePath = '/'; - public bool $cookieSecure = false; - public bool $cookieHTTPOnly = false; - public ?string $cookieSameSite = 'Lax'; public array $proxyIPs = []; public string $CSRFTokenName = 'csrf_test_name'; public string $CSRFHeaderName = 'X-CSRF-TOKEN'; diff --git a/system/Test/Mock/MockCLIConfig.php b/system/Test/Mock/MockCLIConfig.php index 261284887d8d..a1756f41e248 100644 --- a/system/Test/Mock/MockCLIConfig.php +++ b/system/Test/Mock/MockCLIConfig.php @@ -17,12 +17,6 @@ class MockCLIConfig extends App { public string $baseURL = 'http://example.com/'; public string $uriProtocol = 'REQUEST_URI'; - public string $cookiePrefix = ''; - public string $cookieDomain = ''; - public string $cookiePath = '/'; - public bool $cookieSecure = false; - public bool $cookieHTTPOnly = false; - public ?string $cookieSameSite = 'Lax'; public array $proxyIPs = []; public string $CSRFTokenName = 'csrf_test_name'; public string $CSRFCookieName = 'csrf_cookie_name'; From e954b7292641e35cd8a1282cf82979dd67e00484 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 23 Dec 2022 14:01:27 +0900 Subject: [PATCH 036/485] refactor: add @var to specify type --- system/HTTP/Response.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index d1b61e98562f..33cf7542635c 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -15,6 +15,7 @@ use CodeIgniter\Cookie\CookieStore; use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\App; +use Config\Cookie as CookieConfig; use Config\Services; /** @@ -156,7 +157,11 @@ public function __construct($config) $this->CSPEnabled = $config->CSPEnabled; $this->cookieStore = new CookieStore([]); - Cookie::setDefaults(config('Cookie')); + + /** @var CookieConfig $cookie */ + $cookie = config('Cookie'); + + Cookie::setDefaults($cookie); // Default to an HTML Content-Type. Devs can override if needed. $this->setContentType('text/html'); From d3f06e96494daa0495f8a2db1fd08d58d8a0d99a Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 23 Dec 2022 14:06:11 +0900 Subject: [PATCH 037/485] refactor: private method --- system/Security/Security.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/system/Security/Security.php b/system/Security/Security.php index 210e9a71a72d..d0701f8eaa56 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -195,7 +195,10 @@ public function __construct(App $config) } if ($this->isCSRFCookie()) { - $this->configureCookie($config); + /** @var CookieConfig $cookie */ + $cookie = config('Cookie'); + + $this->configureCookie($cookie); } else { // Session based CSRF protection $this->configureSession(); @@ -220,11 +223,8 @@ private function configureSession(): void $this->session = Services::session(); } - private function configureCookie(App $config): void + private function configureCookie(CookieConfig $cookie): void { - /** @var CookieConfig $cookie */ - $cookie = config('Cookie'); - $cookiePrefix = $cookie->prefix; $this->cookieName = $cookiePrefix . $this->rawCookieName; Cookie::setDefaults($cookie); From 2ccf34190003a50af75afe372ee832ddd36c0add Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 23 Dec 2022 14:27:20 +0900 Subject: [PATCH 038/485] test: remove test that are no longer needed --- tests/system/HTTP/ResponseTest.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/system/HTTP/ResponseTest.php b/tests/system/HTTP/ResponseTest.php index 6947d8eea33d..6ad18e7f0e11 100644 --- a/tests/system/HTTP/ResponseTest.php +++ b/tests/system/HTTP/ResponseTest.php @@ -12,7 +12,6 @@ namespace CodeIgniter\HTTP; use CodeIgniter\Config\Factories; -use CodeIgniter\Cookie\Exceptions\CookieException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockResponse; @@ -512,14 +511,4 @@ public function testPretendOutput() $this->assertSame('Happy days', $actual); } - - public function testInvalidSameSiteCookie() - { - $config = new App(); - $config->cookieSameSite = 'Invalid'; - - $this->expectException(CookieException::class); - $this->expectExceptionMessage(lang('Cookie.invalidSameSite', ['Invalid'])); - new Response($config); - } } From e9cc877a98e49494cc7b406ec3dc855b22d34441 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 23 Dec 2022 14:28:43 +0900 Subject: [PATCH 039/485] docs: add @phpstan-var --- app/Config/Cookie.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Config/Cookie.php b/app/Config/Cookie.php index 440af5ee8070..84ccc0e99d80 100644 --- a/app/Config/Cookie.php +++ b/app/Config/Cookie.php @@ -84,6 +84,8 @@ class Cookie extends BaseConfig * Defaults to `Lax` for compatibility with modern browsers. Setting `''` * (empty string) means default SameSite attribute set by browsers (`Lax`) * will be set on cookies. If set to `None`, `$secure` must also be set. + * + * @phpstan-var 'None'|'Lax'|'Strict'|'' */ public string $samesite = 'Lax'; From 85b90d8762b2b90f1dae7c59cd23a326e383deec Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 23 Dec 2022 14:45:11 +0900 Subject: [PATCH 040/485] test: update failed tests --- tests/system/API/ResponseTraitTest.php | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/system/API/ResponseTraitTest.php b/tests/system/API/ResponseTraitTest.php index 0c060691b0b3..40a029dd8d44 100644 --- a/tests/system/API/ResponseTraitTest.php +++ b/tests/system/API/ResponseTraitTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\API; +use CodeIgniter\Config\Factories; use CodeIgniter\Format\FormatterInterface; use CodeIgniter\Format\JSONFormatter; use CodeIgniter\Format\XMLFormatter; @@ -20,6 +21,7 @@ use CodeIgniter\Test\Mock\MockIncomingRequest; use CodeIgniter\Test\Mock\MockResponse; use Config\App; +use Config\Cookie; use stdClass; /** @@ -59,17 +61,25 @@ protected function makeController(array $userConfig = [], string $uri = 'http:// 'negotiateLocale' => false, 'supportedLocales' => ['en'], 'CSPEnabled' => false, - 'cookiePrefix' => '', - 'cookieDomain' => '', - 'cookiePath' => '/', - 'cookieSecure' => false, - 'cookieHTTPOnly' => false, 'proxyIPs' => [], - 'cookieSameSite' => 'Lax', ] as $key => $value) { $config->{$key} = $value; } + $cookie = new Cookie(); + + foreach ([ + 'prefix' => '', + 'domain' => '', + 'path' => '/', + 'secure' => false, + 'httponly' => false, + 'samesite' => 'Lax', + ] as $key => $value) { + $cookie->{$key} = $value; + } + Factories::injectMock('config', 'Cookie', $cookie); + if ($this->request === null) { $this->request = new MockIncomingRequest($config, new URI($uri), null, new UserAgent()); $this->response = new MockResponse($config); From dc47df79df257be62629bb00da24b833714d3f72 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 23 Dec 2022 14:59:03 +0900 Subject: [PATCH 041/485] test: remove test that are no longer needed --- tests/system/CommonFunctionsTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index a0b5280ec6bc..23d7117f7045 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -488,12 +488,9 @@ public function testReallyWritable() public function testSlashItem() { - $this->assertSame('/', slash_item('cookiePath')); // / - $this->assertSame('', slash_item('cookieDomain')); // '' $this->assertSame('en/', slash_item('defaultLocale')); // en $this->assertSame('7200/', slash_item('sessionExpiration')); // int 7200 $this->assertSame('', slash_item('negotiateLocale')); // false - $this->assertSame('1/', slash_item('cookieHTTPOnly')); // true } public function testSlashItemOnInexistentItem() From dc138c272fe6edb6ebb7dd6874899a53b7716007 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 23 Dec 2022 15:01:10 +0900 Subject: [PATCH 042/485] test: update tests --- tests/system/API/ResponseTraitTest.php | 20 +++++++++++----- tests/system/CommonFunctionsTest.php | 24 +++++++++++++------ .../SecurityCSRFSessionRandomizeTokenTest.php | 21 +++++++++++----- .../Security/SecurityCSRFSessionTest.php | 21 +++++++++++----- 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/tests/system/API/ResponseTraitTest.php b/tests/system/API/ResponseTraitTest.php index 40a029dd8d44..e6e18d508776 100644 --- a/tests/system/API/ResponseTraitTest.php +++ b/tests/system/API/ResponseTraitTest.php @@ -542,17 +542,25 @@ public function testFormatByRequestNegotiateIfFormatIsNotJsonOrXML() 'negotiateLocale' => false, 'supportedLocales' => ['en'], 'CSPEnabled' => false, - 'cookiePrefix' => '', - 'cookieDomain' => '', - 'cookiePath' => '/', - 'cookieSecure' => false, - 'cookieHTTPOnly' => false, 'proxyIPs' => [], - 'cookieSameSite' => 'Lax', ] as $key => $value) { $config->{$key} = $value; } + $cookie = new Cookie(); + + foreach ([ + 'prefix' => '', + 'domain' => '', + 'path' => '/', + 'secure' => false, + 'httponly' => false, + 'samesite' => 'Lax', + ] as $key => $value) { + $cookie->{$key} = $value; + } + Factories::injectMock('config', 'Cookie', $cookie); + $request = new MockIncomingRequest($config, new URI($config->baseURL), null, new UserAgent()); $response = new MockResponse($config); diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 23d7117f7045..cf0555fba3d8 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter; use CodeIgniter\Config\BaseService; +use CodeIgniter\Config\Factories; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RedirectResponse; @@ -28,6 +29,7 @@ use CodeIgniter\Test\Mock\MockSession; use CodeIgniter\Test\TestLogger; use Config\App; +use Config\Cookie; use Config\Logger; use Config\Modules; use Config\Services; @@ -511,6 +513,8 @@ public function testSlashItemThrowsErrorOnNonStringableItem() protected function injectSessionMock() { + $appConfig = new App(); + $defaults = [ 'sessionDriver' => FileHandler::class, 'sessionCookieName' => 'ci_session', @@ -519,19 +523,25 @@ protected function injectSessionMock() 'sessionMatchIP' => false, 'sessionTimeToUpdate' => 300, 'sessionRegenerateDestroy' => false, - 'cookieDomain' => '', - 'cookiePrefix' => '', - 'cookiePath' => '/', - 'cookieSecure' => false, - 'cookieSameSite' => 'Lax', ]; - $appConfig = new App(); - foreach ($defaults as $key => $config) { $appConfig->{$key} = $config; } + $cookie = new Cookie(); + + foreach ([ + 'prefix' => '', + 'domain' => '', + 'path' => '/', + 'secure' => false, + 'samesite' => 'Lax', + ] as $key => $value) { + $cookie->{$key} = $value; + } + Factories::injectMock('config', 'Cookie', $cookie); + $session = new MockSession(new FileHandler($appConfig, '127.0.0.1'), $appConfig); $session->setLogger(new TestLogger(new Logger())); BaseService::injectMock('session', $session); diff --git a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php index 6086472d3f4c..f8f3fc01fbaa 100644 --- a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php +++ b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php @@ -26,6 +26,7 @@ use CodeIgniter\Test\Mock\MockSession; use CodeIgniter\Test\TestLogger; use Config\App as AppConfig; +use Config\Cookie; use Config\Logger as LoggerConfig; use Config\Security as SecurityConfig; @@ -75,20 +76,28 @@ private function createSession($options = []): Session 'sessionMatchIP' => false, 'sessionTimeToUpdate' => 300, 'sessionRegenerateDestroy' => false, - 'cookieDomain' => '', - 'cookiePrefix' => '', - 'cookiePath' => '/', - 'cookieSecure' => false, - 'cookieSameSite' => 'Lax', ]; + $config = array_merge($defaults, $options); - $config = array_merge($defaults, $options); $appConfig = new AppConfig(); foreach ($config as $key => $c) { $appConfig->{$key} = $c; } + $cookie = new Cookie(); + + foreach ([ + 'prefix' => '', + 'domain' => '', + 'path' => '/', + 'secure' => false, + 'samesite' => 'Lax', + ] as $key => $value) { + $cookie->{$key} = $value; + } + Factories::injectMock('config', 'Cookie', $cookie); + $session = new MockSession(new ArrayHandler($appConfig, '127.0.0.1'), $appConfig); $session->setLogger(new TestLogger(new LoggerConfig())); diff --git a/tests/system/Security/SecurityCSRFSessionTest.php b/tests/system/Security/SecurityCSRFSessionTest.php index 7fbd7096853f..495f9e58b06d 100644 --- a/tests/system/Security/SecurityCSRFSessionTest.php +++ b/tests/system/Security/SecurityCSRFSessionTest.php @@ -25,6 +25,7 @@ use CodeIgniter\Test\Mock\MockSession; use CodeIgniter\Test\TestLogger; use Config\App as AppConfig; +use Config\Cookie; use Config\Logger as LoggerConfig; use Config\Security as SecurityConfig; @@ -68,20 +69,28 @@ private function createSession($options = []): Session 'sessionMatchIP' => false, 'sessionTimeToUpdate' => 300, 'sessionRegenerateDestroy' => false, - 'cookieDomain' => '', - 'cookiePrefix' => '', - 'cookiePath' => '/', - 'cookieSecure' => false, - 'cookieSameSite' => 'Lax', ]; + $config = array_merge($defaults, $options); - $config = array_merge($defaults, $options); $appConfig = new AppConfig(); foreach ($config as $key => $c) { $appConfig->{$key} = $c; } + $cookie = new Cookie(); + + foreach ([ + 'prefix' => '', + 'domain' => '', + 'path' => '/', + 'secure' => false, + 'samesite' => 'Lax', + ] as $key => $value) { + $cookie->{$key} = $value; + } + Factories::injectMock('config', 'Cookie', $cookie); + $session = new MockSession(new ArrayHandler($appConfig, '127.0.0.1'), $appConfig); $session->setLogger(new TestLogger(new LoggerConfig())); From 01f7a6b7bd19aeb73dc5951da4d0788cb5cf125e Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 4 Feb 2023 12:53:54 +0900 Subject: [PATCH 043/485] docs: add docs --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + .../source/installation/upgrade_440.rst | 63 +++++++++++++++++++ .../source/installation/upgrading.rst | 1 + 3 files changed, 65 insertions(+) create mode 100644 user_guide_src/source/installation/upgrade_440.rst diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index a685e948e630..c80d12602c98 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -74,6 +74,7 @@ Message Changes Changes ******* +- **Config:** The deprecated Cookie items in **app/Config/App.php** has been removed. - **DownloadResponse:** When generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. Deprecations diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst new file mode 100644 index 000000000000..16995c1d897e --- /dev/null +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -0,0 +1,63 @@ +############################## +Upgrading from 4.3.x to 4.4.0 +############################## + +Please refer to the upgrade instructions corresponding to your installation method. + +- :ref:`Composer Installation App Starter Upgrading ` +- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading ` +- :ref:`Manual Installation Upgrading ` + +.. contents:: + :local: + :depth: 2 + +Breaking Changes +**************** + +Mandatory File Changes +********************** + +Config Files +============ + +app/Config/Cookie.php +--------------------- + +The Cookie config items in **app/Config/App.php** are no longer used. + +1. Copy **app/Config/Cookie.php** from the new framework to your **app/Config** + directory, and configure it. +2. Remove the properties (from ``$cookiePrefix`` to ``$cookieSameSite``) in + **app/Config/App.php**. + +Breaking Enhancements +********************* + +Project Files +************* + +Some 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 `_. + +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: + +Config +------ + +- @TODO + +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 effect on the runtime: + +- @TODO diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index d754d020b55c..6ac83fd4c239 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -16,6 +16,7 @@ See also :doc:`./backward_compatibility_notes`. backward_compatibility_notes + upgrade_440 upgrade_432 upgrade_431 upgrade_430 From 2f71c7a43619b27b041df0a28f8f257edba3b1c9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 9 Feb 2023 08:21:22 +0900 Subject: [PATCH 044/485] fix: deprecate $request and $response in Exceptions::__construct() When registering Exception Handler, Request object was generated. But it was too early, and difficult to understand. --- system/Config/Services.php | 6 +++++- system/Debug/Exceptions.php | 12 ++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/system/Config/Services.php b/system/Config/Services.php index a2d13c40e80c..2358e87bb1a5 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -250,6 +250,8 @@ public static function encrypter(?EncryptionConfig $config = null, $getShared = * - register_shutdown_function * * @return Exceptions + * + * @deprecated The parameter $request and $response are deprecated. */ public static function exceptions( ?ExceptionsConfig $config = null, @@ -262,7 +264,9 @@ public static function exceptions( } $config ??= config('Exceptions'); - $request ??= AppServices::request(); + /** @var ExceptionsConfig $config */ + + // @TODO remove instantiation of Response in the future. $response ??= AppServices::response(); return new Exceptions($config, $request, $response); diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 6dc098d4af10..d4e80f28f2e9 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -21,6 +21,7 @@ use CodeIgniter\HTTP\ResponseInterface; use Config\Exceptions as ExceptionsConfig; use Config\Paths; +use Config\Services; use ErrorException; use Psr\Log\LogLevel; use Throwable; @@ -71,15 +72,15 @@ class Exceptions private ?Throwable $exceptionCaughtByExceptionHandler = null; /** - * @param CLIRequest|IncomingRequest $request + * @param CLIRequest|IncomingRequest|null $request + * + * @deprecated The parameter $request and $response are deprecated. No longer used. */ - public function __construct(ExceptionsConfig $config, $request, ResponseInterface $response) + public function __construct(ExceptionsConfig $config, $request, ResponseInterface $response) /** @phpstan-ignore-line */ { $this->ob_level = ob_get_level(); $this->viewPath = rtrim($config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR; $this->config = $config; - $this->request = $request; - $this->response = $response; // workaround for upgraded users // This causes "Deprecated: Creation of dynamic property" in PHP 8.2. @@ -119,6 +120,9 @@ public function exceptionHandler(Throwable $exception) [$statusCode, $exitCode] = $this->determineCodes($exception); + $this->request = Services::request(); + $this->response = Services::response(); + if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { log_message('critical', "{message}\nin {exFile} on line {exLine}.\n{trace}", [ 'message' => $exception->getMessage(), From 044460855855ba9df308a520c5e6cb16f759b8d1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 9 Feb 2023 08:46:23 +0900 Subject: [PATCH 045/485] chore: add rule to skip --- rector.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rector.php b/rector.php index 797387ba4aba..0473a5d8cfdb 100644 --- a/rector.php +++ b/rector.php @@ -28,6 +28,7 @@ use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector; use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector; use Rector\DeadCode\Rector\If_\UnwrapFutureCompatibleIfPhpVersionRector; use Rector\DeadCode\Rector\MethodCall\RemoveEmptyMethodCallRector; @@ -89,6 +90,11 @@ __DIR__ . '/tests/system/Test/ReflectionHelperTest.php', ], + RemoveUnusedConstructorParamRector::class => [ + // there are deprecated parameters + __DIR__ . '/system/Debug/Exceptions.php', + ], + // call on purpose for nothing happen check RemoveEmptyMethodCallRector::class => [ __DIR__ . '/tests', From 08a6a7852dbe4aacafb206524b68c0c940889059 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 9 Feb 2023 09:23:23 +0900 Subject: [PATCH 046/485] test: fix incorrect setup baseURL should not be empty. --- tests/system/Helpers/FormHelperTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php index 9a525adf7ad2..b45329c8663c 100644 --- a/tests/system/Helpers/FormHelperTest.php +++ b/tests/system/Helpers/FormHelperTest.php @@ -39,7 +39,6 @@ private function setRequest(): void Services::injectMock('uri', $uri); $config = new App(); - $config->baseURL = ''; $config->indexPage = 'index.php'; $request = Services::request($config); From c4efd78fa96c4b3fe543d6abbcc3a77bcab0d517 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 3 Mar 2022 13:29:05 +0900 Subject: [PATCH 047/485] feat: add Entity::injectRawData() to avoid name collision --- system/Database/MySQLi/Result.php | 2 +- system/Database/OCI8/Result.php | 2 +- system/Database/Postgre/Result.php | 2 +- system/Database/SQLSRV/Result.php | 2 +- system/Database/SQLite3/Result.php | 2 +- system/Entity/Entity.php | 16 ++++++++++++++-- tests/system/Entity/EntityTest.php | 30 ++++++++++++++++++++++++++++++ 7 files changed, 49 insertions(+), 7 deletions(-) diff --git a/system/Database/MySQLi/Result.php b/system/Database/MySQLi/Result.php index 550fc0abf835..90bf314964c5 100644 --- a/system/Database/MySQLi/Result.php +++ b/system/Database/MySQLi/Result.php @@ -146,7 +146,7 @@ protected function fetchAssoc() protected function fetchObject(string $className = 'stdClass') { if (is_subclass_of($className, Entity::class)) { - return empty($data = $this->fetchAssoc()) ? false : (new $className())->setAttributes($data); + return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($data); } return $this->resultID->fetch_object($className); diff --git a/system/Database/OCI8/Result.php b/system/Database/OCI8/Result.php index cb192de325f1..f20dcaddb88e 100644 --- a/system/Database/OCI8/Result.php +++ b/system/Database/OCI8/Result.php @@ -102,7 +102,7 @@ protected function fetchObject(string $className = 'stdClass') return $row; } if (is_subclass_of($className, Entity::class)) { - return (new $className())->setAttributes((array) $row); + return (new $className())->injectRawData((array) $row); } $instance = new $className(); diff --git a/system/Database/Postgre/Result.php b/system/Database/Postgre/Result.php index c3fce412223d..ebda4f9e9b02 100644 --- a/system/Database/Postgre/Result.php +++ b/system/Database/Postgre/Result.php @@ -112,7 +112,7 @@ protected function fetchAssoc() protected function fetchObject(string $className = 'stdClass') { if (is_subclass_of($className, Entity::class)) { - return empty($data = $this->fetchAssoc()) ? false : (new $className())->setAttributes($data); + return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($data); } return pg_fetch_object($this->resultID, null, $className); diff --git a/system/Database/SQLSRV/Result.php b/system/Database/SQLSRV/Result.php index 6e172d8ac770..541dbe34d31e 100755 --- a/system/Database/SQLSRV/Result.php +++ b/system/Database/SQLSRV/Result.php @@ -152,7 +152,7 @@ protected function fetchAssoc() protected function fetchObject(string $className = 'stdClass') { if (is_subclass_of($className, Entity::class)) { - return empty($data = $this->fetchAssoc()) ? false : (new $className())->setAttributes($data); + return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($data); } return sqlsrv_fetch_object($this->resultID, $className); diff --git a/system/Database/SQLite3/Result.php b/system/Database/SQLite3/Result.php index 05994469e5eb..3f6169970571 100644 --- a/system/Database/SQLite3/Result.php +++ b/system/Database/SQLite3/Result.php @@ -140,7 +140,7 @@ protected function fetchObject(string $className = 'stdClass') $classObj = new $className(); if (is_subclass_of($className, Entity::class)) { - return $classObj->setAttributes($row); + return $classObj->injectRawData($row); } $classSet = Closure::bind(function ($key, $value) { diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 92cedeff213f..5220ae5b0aa8 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -279,7 +279,7 @@ public function hasChanged(?string $key = null): bool * * @return $this */ - public function setAttributes(array $data) + public function injectRawData(array $data) { $this->attributes = $data; @@ -288,6 +288,18 @@ public function setAttributes(array $data) return $this; } + /** + * Set raw data array without any mutations + * + * @return $this + * + * @deprecated Use injectRawData() instead. + */ + public function setAttributes(array $data) + { + return $this->injectRawData($data); + } + /** * Checks the datamap to see if this property name is being mapped, * and returns the db column name, if any, or the original property name. @@ -449,7 +461,7 @@ public function __set(string $key, $value = null) // so maybe wants to do sth with null value automatically $method = 'set' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key))); - if (method_exists($this, $method)) { + if (method_exists($this, $method) && $method !== 'setAttributes') { $this->{$method}($value); return $this; diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 1e03157ab188..79aeb53f3199 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -34,6 +34,36 @@ final class EntityTest extends CIUnitTestCase { use ReflectionHelper; + public function testSetStringToPropertyNamedAttributes() + { + $entity = $this->getEntity(); + + $entity->attributes = 'attributes'; + + $this->assertSame('attributes', $entity->attributes); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues + */ + public function testSetArrayToPropertyNamedAttributes() + { + $entity = new Entity(); + + $entity->a = 1; + $entity->attributes = [1, 2, 3]; + + $expected = [ + 'a' => 1, + 'attributes' => [ + 0 => 1, + 1 => 2, + 2 => 3, + ], + ]; + $this->assertSame($expected, $entity->toRawArray()); + } + public function testSimpleSetAndGet() { $entity = $this->getEntity(); From 63a00bbfdb676c33afc3911e061cb852e3ccd386 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 11 Feb 2023 10:53:41 +0900 Subject: [PATCH 048/485] docs: add changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index dcb705a9fa15..965a0efbc057 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -83,6 +83,8 @@ Changes Deprecations ************ +- **Entity:** ``Entity::setAttributes()`` is deprecated. Use ``Entity::injectRawData()`` instead. + Bugs Fixed ********** From 5a3ac57fe6a19ea18baaa0c10ceddeeea77f39d3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 14 Feb 2023 13:35:42 +0900 Subject: [PATCH 049/485] fix: URI::setSegment() accepts the last +2 segment without Exception --- system/HTTP/URI.php | 10 +++++++--- tests/system/HTTP/URITest.php | 19 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 587e441ffc17..5615bb8d1287 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -562,9 +562,9 @@ public function getSegment(int $number, string $default = ''): string */ public function setSegment(int $number, $value) { - // The segment should treat the array as 1-based for the user - // but we still have to deal with a zero-based array. - $number--; + if ($number < 1) { + throw HTTPException::forURISegmentOutOfRange($number); + } if ($number > count($this->segments) + 1) { if ($this->silent) { @@ -574,6 +574,10 @@ public function setSegment(int $number, $value) throw HTTPException::forURISegmentOutOfRange($number); } + // The segment should treat the array as 1-based for the user + // but we still have to deal with a zero-based array. + $number--; + $this->segments[$number] = $value; $this->refreshPath(); diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index 14de6e3d587b..87cb5253a735 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -808,32 +808,37 @@ public function testSetSegment() $this->assertSame('foo/banana/baz', $uri->getPath()); } - public function testSetSegmentFallback() + public function testSetSegmentNewOne() { $base = 'http://example.com'; + $uri = new URI($base); - $uri = new URI($base); + // Can set the next segment. $uri->setSegment(1, 'first'); - $uri->setSegment(3, 'third'); + // Can set the next segment. + $uri->setSegment(2, 'third'); $this->assertSame('first/third', $uri->getPath()); + // Can replace the existing segment. $uri->setSegment(2, 'second'); $this->assertSame('first/second', $uri->getPath()); + // Can set the next segment. $uri->setSegment(3, 'third'); $this->assertSame('first/second/third', $uri->getPath()); - $uri->setSegment(5, 'fifth'); + // Can set the next segment. + $uri->setSegment(4, 'fourth'); - $this->assertSame('first/second/third/fifth', $uri->getPath()); + $this->assertSame('first/second/third/fourth', $uri->getPath()); - // sixth or seventh was not set + // Cannot set the next next segment. $this->expectException(HTTPException::class); - $uri->setSegment(8, 'eighth'); + $uri->setSegment(6, 'six'); } public function testSetBadSegment() From d4e7426bc32fc9c6530d038d778d73bb1a9625bb Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 11:37:44 +0900 Subject: [PATCH 050/485] docs: add changelog and upgrading guide --- user_guide_src/source/changelogs/v4.4.0.rst | 9 +++++++++ user_guide_src/source/installation/upgrade_440.rst | 13 +++++++++++++ .../source/installation/upgrade_440/002.php | 12 ++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 user_guide_src/source/installation/upgrade_440/002.php diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 965a0efbc057..a4d2afbafec2 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -20,6 +20,15 @@ BREAKING Behavior Changes ================ +URI::setSegment() and Non-Existent Segment +------------------------------------------ + +An exception is now thrown when you set the last ``+2`` segment. +In previous versions, an exception was thrown only if the last segment ``+3`` +or more was specified. See :ref:`upgrade-440-uri-setsegment`. + +The next segment (``+1``) of the current last segment can be set as before. + Interface Changes ================= diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 16995c1d897e..f5bc7aef91f2 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -15,6 +15,19 @@ Please refer to the upgrade instructions corresponding to your installation meth Breaking Changes **************** +.. _upgrade-440-uri-setsegment: + +URI::setSegment() Change +======================== + +Dut to a bug, in previous versions an exception was not thrown if the last segment +``+2`` was specified. This bug has been fixed. + +If your code depends on this bug, fix the segment number. + +.. literalinclude:: upgrade_440/002.php + :lines: 2- + Mandatory File Changes ********************** diff --git a/user_guide_src/source/installation/upgrade_440/002.php b/user_guide_src/source/installation/upgrade_440/002.php new file mode 100644 index 000000000000..9cabdfcfcff3 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_440/002.php @@ -0,0 +1,12 @@ +setSegment(4, 'three'); +// The URI will be http://example.com/one/two/three + +// After: +$uri->setSegment(4, 'three'); // Will throw Exception +$uri->setSegment(3, 'three'); +// The URI will be http://example.com/one/two/three From d44812d91d9fbec9c9d742ad759fa08cd405bd57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Thu, 16 Feb 2023 23:50:34 +0800 Subject: [PATCH 051/485] Enhance: [MySQLi] use MYSQLI_OPT_INT_AND_FLOAT_NATIVE. --- system/Database/MySQLi/Connection.php | 1 + 1 file changed, 1 insertion(+) diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 094fcfe7e3ec..26ece9a5d44b 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -98,6 +98,7 @@ public function connect(bool $persistent = false) mysqli_report(MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX); $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10); + $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1); if (isset($this->strictOn)) { if ($this->strictOn) { From f1f5a5139dc8ac9ef706cbd699402800ec1bb1c2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 17 Feb 2023 09:38:24 +0900 Subject: [PATCH 052/485] docs: update PHPDocs --- system/HTTP/URI.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 5615bb8d1287..3d673fab0928 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -532,12 +532,14 @@ public function getSegments(): array /** * Returns the value of a specific segment of the URI path. + * Allows to get only existing segments or the next one. * - * @param int $number Segment number + * @param int $number Segment number starting at 1 * @param string $default Default value * - * @return string The value of the segment. If no segment is found, - * throws InvalidArgumentError + * @return string The value of the segment. If you specify the last +1 + * segment, the $default value. If you specify the last +2 + * or more throws HTTPException. */ public function getSegment(int $number, string $default = ''): string { @@ -556,7 +558,8 @@ public function getSegment(int $number, string $default = ''): string * Set the value of a specific segment of the URI path. * Allows to set only existing segments or add new one. * - * @param mixed $value (string or int) + * @param int $number Segment number starting at 1 + * @param int|string $value * * @return $this */ From 65cd0452cb70f55db0a87a59157b004d14b61b3c Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 17 Feb 2023 09:40:21 +0900 Subject: [PATCH 053/485] fix: incorrect segment number in Exception message --- system/HTTP/URI.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 3d673fab0928..4a0c75ce245d 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -543,14 +543,15 @@ public function getSegments(): array */ public function getSegment(int $number, string $default = ''): string { - // The segment should treat the array as 1-based for the user - // but we still have to deal with a zero-based array. - $number--; - if ($number > count($this->segments) && ! $this->silent) { + if ($number > count($this->segments) + 1 && ! $this->silent) { throw HTTPException::forURISegmentOutOfRange($number); } + // The segment should treat the array as 1-based for the user + // but we still have to deal with a zero-based array. + $number--; + return $this->segments[$number] ?? $default; } From 391e2b9e4616ac1e42dfa578f6adb0d3dbaf2644 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 17 Feb 2023 09:40:56 +0900 Subject: [PATCH 054/485] fix: throw an exception for an impossible segment number --- system/HTTP/URI.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 4a0c75ce245d..7e57e4aa888c 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -543,6 +543,9 @@ public function getSegments(): array */ public function getSegment(int $number, string $default = ''): string { + if ($number < 1) { + throw HTTPException::forURISegmentOutOfRange($number); + } if ($number > count($this->segments) + 1 && ! $this->silent) { throw HTTPException::forURISegmentOutOfRange($number); From 47833d2b1a55bc3748f8bd818f703f91d6de7c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Fri, 17 Feb 2023 23:11:00 +0800 Subject: [PATCH 055/485] Enhance:[MySQLi] Use the option to define whether MYSQLI_OPT_INT_AND_FLOAT_NATIVE needs to be used. --- app/Config/Database.php | 35 ++++++++++++++------------- system/Database/BaseConnection.php | 8 ++++++ system/Database/MySQLi/Connection.php | 5 +++- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/app/Config/Database.php b/app/Config/Database.php index 2c092124550f..44515c4d5ecc 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -25,23 +25,24 @@ class Database extends Config * The default database connection. */ public array $default = [ - 'DSN' => '', - 'hostname' => 'localhost', - 'username' => '', - 'password' => '', - 'database' => '', - 'DBDriver' => 'MySQLi', - 'DBPrefix' => '', - 'pConnect' => false, - 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', - 'swapPre' => '', - 'encrypt' => false, - 'compress' => false, - 'strictOn' => false, - 'failover' => [], - 'port' => 3306, + 'DSN' => '', + 'hostname' => 'localhost', + 'username' => '', + 'password' => '', + 'database' => '', + 'DBDriver' => 'MySQLi', + 'DBPrefix' => '', + 'pConnect' => false, + 'DBDebug' => true, + 'charset' => 'utf8', + 'DBCollat' => 'utf8_general_ci', + 'swapPre' => '', + 'encrypt' => false, + 'compress' => false, + 'strictOn' => false, + 'failover' => [], + 'port' => 3306, + 'DBTypeNative' => false, ]; /** diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index badd34d5c077..5d79c8c052eb 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -29,6 +29,7 @@ * @property string $DBDriver * @property string $DBPrefix * @property string $DSN + * @property bool $DBTypeNative * @property array|bool $encrypt * @property array $failover * @property string $hostname @@ -339,6 +340,13 @@ abstract class BaseConnection implements ConnectionInterface */ protected $queryClass = Query::class; + /** + * Use MYSQLI_OPT_INT_AND_FLOAT_NATIVE + * + * @var bool + */ + protected $DBTypeNative = false; + /** * Saves our connection settings. */ diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 26ece9a5d44b..f6cbe83dd604 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -98,7 +98,10 @@ public function connect(bool $persistent = false) mysqli_report(MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX); $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10); - $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1); + + if(isset($this->DBTypeNative) && $this->DBTypeNative){ + $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE,1); + } if (isset($this->strictOn)) { if ($this->strictOn) { From be1eab9c4f39b2bf3e826b55ec744a7ff23463ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Sat, 18 Feb 2023 21:42:57 +0800 Subject: [PATCH 056/485] fix:Rename the DBTypeNative to numberNative and move the property to MySQLi for use. --- app/Config/Database.php | 2 +- system/Database/BaseConnection.php | 8 -------- system/Database/MySQLi/Connection.php | 11 +++++++++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app/Config/Database.php b/app/Config/Database.php index 44515c4d5ecc..e2450ec16cf1 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -42,7 +42,7 @@ class Database extends Config 'strictOn' => false, 'failover' => [], 'port' => 3306, - 'DBTypeNative' => false, + 'numberNative' => false, ]; /** diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 5d79c8c052eb..badd34d5c077 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -29,7 +29,6 @@ * @property string $DBDriver * @property string $DBPrefix * @property string $DSN - * @property bool $DBTypeNative * @property array|bool $encrypt * @property array $failover * @property string $hostname @@ -340,13 +339,6 @@ abstract class BaseConnection implements ConnectionInterface */ protected $queryClass = Query::class; - /** - * Use MYSQLI_OPT_INT_AND_FLOAT_NATIVE - * - * @var bool - */ - protected $DBTypeNative = false; - /** * Saves our connection settings. */ diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index f6cbe83dd604..f657734c896a 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -72,6 +72,13 @@ class Connection extends BaseConnection */ public $resultMode = MYSQLI_STORE_RESULT; + /** + * Use MYSQLI_OPT_INT_AND_FLOAT_NATIVE + * + * @var bool + */ + public $numberNative = false; + /** * Connect to the database. * @@ -99,8 +106,8 @@ public function connect(bool $persistent = false) $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10); - if(isset($this->DBTypeNative) && $this->DBTypeNative){ - $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE,1); + if (isset($this->numberNative) && $this->numberNative) { + $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1); } if (isset($this->strictOn)) { From 7b756d1fbb1e18d814474d521ed16f43b3f70dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Sat, 18 Feb 2023 22:34:03 +0800 Subject: [PATCH 057/485] Enhance:Add numberNative to configuration.rst to specify whether MYSQLI_OPT_INT_AND_FLOAT_NATIVE is enabled or not. --- user_guide_src/source/database/configuration.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/database/configuration.rst b/user_guide_src/source/database/configuration.rst index b5a768439ccc..33a069800d29 100644 --- a/user_guide_src/source/database/configuration.rst +++ b/user_guide_src/source/database/configuration.rst @@ -150,6 +150,7 @@ Explanation of Values: See `SQLite documentation `_. To enforce Foreign Key constraint, set this config item to true. **busyTimeout** milliseconds (int) - Sleeps for a specified amount of time when a table is locked (``SQLite3`` only). +**numberNative** true/false (boolean) - Whether or not to enable MYSQLI_OPT_INT_AND_FLOAT_NATIVE (``MySQLi`` only). =============== =========================================================================================================== .. note:: Depending on what database driver you are using (``MySQLi``, ``Postgres``, From 4ff2ffea507ad84d4c549b13b7add21553faf579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Sat, 18 Feb 2023 22:39:13 +0800 Subject: [PATCH 058/485] fix:Configuration.rst typesetting corrections. --- .../source/database/configuration.rst | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/user_guide_src/source/database/configuration.rst b/user_guide_src/source/database/configuration.rst index 33a069800d29..0c16803d7164 100644 --- a/user_guide_src/source/database/configuration.rst +++ b/user_guide_src/source/database/configuration.rst @@ -108,50 +108,50 @@ default group's configuration settings. The values should be name following this Explanation of Values: ********************** -=============== =========================================================================================================== - Name Config Description -=============== =========================================================================================================== -**dsn** The DSN connect string (an all-in-one configuration sequence). -**hostname** The hostname of your database server. Often this is 'localhost'. -**username** The username used to connect to the database. (``SQLite3`` does not use this.) -**password** The password used to connect to the database. (``SQLite3`` does not use this.) -**database** The name of the database you want to connect to. - - .. note:: CodeIgniter doesn't support dots (``.``) in the database, table, and column names. -**DBDriver** The database driver name. e.g.,: ``MySQLi``, ``Postgres``, etc. The case must match the driver name. - You can set a fully qualified classname to use your custom driver. -**DBPrefix** An optional table prefix which will added to the table name when running - :doc:`Query Builder ` queries. This permits multiple CodeIgniter - installations to share one database. -**pConnect** true/false (boolean) - Whether to use a persistent connection. -**DBDebug** true/false (boolean) - Whether to throw exceptions or not when database errors occur. -**charset** The character set used in communicating with the database. -**DBCollat** The character collation used in communicating with the database (``MySQLi`` only). -**swapPre** A default table prefix that should be swapped with ``DBPrefix``. This is useful for distributed - applications where you might run manually written queries, and need the prefix to still be - customizable by the end user. -**schema** The database schema, default value varies by driver. (Used by ``Postgres`` and ``SQLSRV``.) -**encrypt** Whether or not to use an encrypted connection. - ``SQLSRV`` driver accepts true/false - ``MySQLi`` driver accepts an array with the following options: - * ``ssl_key`` - Path to the private key file - * ``ssl_cert`` - Path to the public key certificate file - * ``ssl_ca`` - Path to the certificate authority file - * ``ssl_capath`` - Path to a directory containing trusted CA certificates in PEM format - * ``ssl_cipher`` - List of *allowed* ciphers to be used for the encryption, separated by colons (``:``) - * ``ssl_verify`` - true/false; Whether to verify the server certificate or not (``MySQLi`` only) -**compress** Whether or not to use client compression (``MySQLi`` only). -**strictOn** true/false (boolean) - Whether to force "Strict Mode" connections, good for ensuring strict SQL - while developing an application (``MySQLi`` only). -**port** The database port number. -**foreignKeys** true/false (boolean) - Whether or not to enable Foreign Key constraint (``SQLite3`` only). - - .. important:: SQLite3 Foreign Key constraint is disabled by default. - See `SQLite documentation `_. - To enforce Foreign Key constraint, set this config item to true. -**busyTimeout** milliseconds (int) - Sleeps for a specified amount of time when a table is locked (``SQLite3`` only). +=============== =========================================================================================================== + Name Config Description +=============== =========================================================================================================== +**dsn** The DSN connect string (an all-in-one configuration sequence). +**hostname** The hostname of your database server. Often this is 'localhost'. +**username** The username used to connect to the database. (``SQLite3`` does not use this.) +**password** The password used to connect to the database. (``SQLite3`` does not use this.) +**database** The name of the database you want to connect to. + + .. note:: CodeIgniter doesn't support dots (``.``) in the database, table, and column names. +**DBDriver** The database driver name. e.g.,: ``MySQLi``, ``Postgres``, etc. The case must match the driver name. + You can set a fully qualified classname to use your custom driver. +**DBPrefix** An optional table prefix which will added to the table name when running + :doc:`Query Builder ` queries. This permits multiple CodeIgniter + installations to share one database. +**pConnect** true/false (boolean) - Whether to use a persistent connection. +**DBDebug** true/false (boolean) - Whether to throw exceptions or not when database errors occur. +**charset** The character set used in communicating with the database. +**DBCollat** The character collation used in communicating with the database (``MySQLi`` only). +**swapPre** A default table prefix that should be swapped with ``DBPrefix``. This is useful for distributed + applications where you might run manually written queries, and need the prefix to still be + customizable by the end user. +**schema** The database schema, default value varies by driver. (Used by ``Postgres`` and ``SQLSRV``.) +**encrypt** Whether or not to use an encrypted connection. + ``SQLSRV`` driver accepts true/false + ``MySQLi`` driver accepts an array with the following options: + * ``ssl_key`` - Path to the private key file + * ``ssl_cert`` - Path to the public key certificate file + * ``ssl_ca`` - Path to the certificate authority file + * ``ssl_capath`` - Path to a directory containing trusted CA certificates in PEM format + * ``ssl_cipher`` - List of *allowed* ciphers to be used for the encryption, separated by colons (``:``) + * ``ssl_verify`` - true/false; Whether to verify the server certificate or not (``MySQLi`` only) +**compress** Whether or not to use client compression (``MySQLi`` only). +**strictOn** true/false (boolean) - Whether to force "Strict Mode" connections, good for ensuring strict SQL + while developing an application (``MySQLi`` only). +**port** The database port number. +**foreignKeys** true/false (boolean) - Whether or not to enable Foreign Key constraint (``SQLite3`` only). + + .. important:: SQLite3 Foreign Key constraint is disabled by default. + See `SQLite documentation `_. + To enforce Foreign Key constraint, set this config item to true. +**busyTimeout** milliseconds (int) - Sleeps for a specified amount of time when a table is locked (``SQLite3`` only). **numberNative** true/false (boolean) - Whether or not to enable MYSQLI_OPT_INT_AND_FLOAT_NATIVE (``MySQLi`` only). -=============== =========================================================================================================== +=============== =========================================================================================================== .. note:: Depending on what database driver you are using (``MySQLi``, ``Postgres``, etc.) not all values will be needed. For example, when using ``SQLite3`` you From d017a0b6c5cd27da4cd1d01ac43861067dc0025e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Sat, 18 Feb 2023 23:39:37 +0800 Subject: [PATCH 059/485] Enhance:Add and complete test files for NumberNative. --- app/Config/Database.php | 39 ++++---- .../Database/Live/MySQLi/NumberNativeTest.php | 88 +++++++++++++++++++ 2 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 tests/system/Database/Live/MySQLi/NumberNativeTest.php diff --git a/app/Config/Database.php b/app/Config/Database.php index e2450ec16cf1..9e752027feed 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -50,25 +50,26 @@ class Database extends Config * running PHPUnit database tests. */ public array $tests = [ - 'DSN' => '', - 'hostname' => '127.0.0.1', - 'username' => '', - 'password' => '', - 'database' => ':memory:', - 'DBDriver' => 'SQLite3', - 'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS - 'pConnect' => false, - 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', - 'swapPre' => '', - 'encrypt' => false, - 'compress' => false, - 'strictOn' => false, - 'failover' => [], - 'port' => 3306, - 'foreignKeys' => true, - 'busyTimeout' => 1000, + 'DSN' => '', + 'hostname' => '127.0.0.1', + 'username' => '', + 'password' => '', + 'database' => ':memory:', + 'DBDriver' => 'SQLite3', + 'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS + 'pConnect' => false, + 'DBDebug' => true, + 'charset' => 'utf8', + 'DBCollat' => 'utf8_general_ci', + 'swapPre' => '', + 'encrypt' => false, + 'compress' => false, + 'strictOn' => false, + 'failover' => [], + 'port' => 3306, + 'foreignKeys' => true, + 'busyTimeout' => 1000, + 'numberNative' => false, ]; public function __construct() diff --git a/tests/system/Database/Live/MySQLi/NumberNativeTest.php b/tests/system/Database/Live/MySQLi/NumberNativeTest.php new file mode 100644 index 000000000000..eb2608b5c751 --- /dev/null +++ b/tests/system/Database/Live/MySQLi/NumberNativeTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\Live\MySQLi; + +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\DatabaseTestTrait; +use Config\Database; +use Tests\Support\Database\Seeds\CITestSeeder; + +/** + * @group DatabaseLive + * + * @internal + */ +final class NumberNativeTest extends CIUnitTestCase +{ + use DatabaseTestTrait; + + private $tests; + protected $refresh = true; + protected $seed = CITestSeeder::class; + + protected function setUp(): void + { + parent::setUp(); + + $config = config('Database'); + + $this->tests = $config->tests; + + $this->tests['DBDriver'] = 'MySQLi'; + } + + public function testEnableNumberNative() + { + $this->tests['numberNative'] = true; + + $db1 = Database::connect($this->tests); + + $this->assertTrue($db1->numberNative); + } + + public function testDisableNumberNative() + { + $this->tests['numberNative'] = false; + + $db1 = Database::connect($this->tests); + + $this->assertFalse($db1->numberNative); + } + + public function testQueryDataAfterEnableNumberNative() + { + $this->tests['numberNative'] = true; + + $db1 = Database::connect($this->tests); + + $data = $db1->table('db_type_test') + ->get() + ->getRow(); + + $this->assertIsFloat($data->type_float); + $this->assertIsInt($data->type_integer); + } + + public function testQueryDataAfterDisableNumberNative() + { + $this->tests['numberNative'] = false; + + $db1 = Database::connect($this->tests); + + $data = $db1->table('db_type_test') + ->get() + ->getRow(); + + $this->assertIsString($data->type_float); + $this->assertIsString($data->type_integer); + } +} From 1cd3415c00af19e441d9a43d31e40562f0255b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Sun, 19 Feb 2023 12:47:08 +0800 Subject: [PATCH 060/485] fix:NumberNativeTest.php adds DBDriver's judgment test and fixes MySQLi's isset judgment for numberNative. --- system/Database/MySQLi/Connection.php | 4 ++-- .../Database/Live/MySQLi/NumberNativeTest.php | 18 ++++++++++++++++-- .../source/database/configuration.rst | 8 ++++---- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index f657734c896a..00c165dbef51 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -106,8 +106,8 @@ public function connect(bool $persistent = false) $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10); - if (isset($this->numberNative) && $this->numberNative) { - $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1); + if($this->numberNative === true){ + $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE,1); } if (isset($this->strictOn)) { diff --git a/tests/system/Database/Live/MySQLi/NumberNativeTest.php b/tests/system/Database/Live/MySQLi/NumberNativeTest.php index eb2608b5c751..1f95dd4e7c59 100644 --- a/tests/system/Database/Live/MySQLi/NumberNativeTest.php +++ b/tests/system/Database/Live/MySQLi/NumberNativeTest.php @@ -36,8 +36,6 @@ protected function setUp(): void $config = config('Database'); $this->tests = $config->tests; - - $this->tests['DBDriver'] = 'MySQLi'; } public function testEnableNumberNative() @@ -46,6 +44,10 @@ public function testEnableNumberNative() $db1 = Database::connect($this->tests); + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + $this->assertTrue($db1->numberNative); } @@ -55,6 +57,10 @@ public function testDisableNumberNative() $db1 = Database::connect($this->tests); + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + $this->assertFalse($db1->numberNative); } @@ -64,6 +70,10 @@ public function testQueryDataAfterEnableNumberNative() $db1 = Database::connect($this->tests); + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + $data = $db1->table('db_type_test') ->get() ->getRow(); @@ -78,6 +88,10 @@ public function testQueryDataAfterDisableNumberNative() $db1 = Database::connect($this->tests); + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + $data = $db1->table('db_type_test') ->get() ->getRow(); diff --git a/user_guide_src/source/database/configuration.rst b/user_guide_src/source/database/configuration.rst index 0c16803d7164..7da92aac9139 100644 --- a/user_guide_src/source/database/configuration.rst +++ b/user_guide_src/source/database/configuration.rst @@ -108,9 +108,9 @@ default group's configuration settings. The values should be name following this Explanation of Values: ********************** -=============== =========================================================================================================== +================ =========================================================================================================== Name Config Description -=============== =========================================================================================================== +================ =========================================================================================================== **dsn** The DSN connect string (an all-in-one configuration sequence). **hostname** The hostname of your database server. Often this is 'localhost'. **username** The username used to connect to the database. (``SQLite3`` does not use this.) @@ -150,8 +150,8 @@ Explanation of Values: See `SQLite documentation `_. To enforce Foreign Key constraint, set this config item to true. **busyTimeout** milliseconds (int) - Sleeps for a specified amount of time when a table is locked (``SQLite3`` only). -**numberNative** true/false (boolean) - Whether or not to enable MYSQLI_OPT_INT_AND_FLOAT_NATIVE (``MySQLi`` only). -=============== =========================================================================================================== +**numberNative** milliseconds (int) - Sleeps for a specified amount of time when a table is locked (``SQLite3`` only). +================ =========================================================================================================== .. note:: Depending on what database driver you are using (``MySQLi``, ``Postgres``, etc.) not all values will be needed. For example, when using ``SQLite3`` you From 7b152dfbd85aecbb60e451d585aa52eb0f81add7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Sun, 19 Feb 2023 12:49:51 +0800 Subject: [PATCH 061/485] fix:Column description of numberNative in CodeIgniter4/user_guide_src/source/database/configuration.rst. --- user_guide_src/source/database/configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/database/configuration.rst b/user_guide_src/source/database/configuration.rst index 7da92aac9139..1683ce801518 100644 --- a/user_guide_src/source/database/configuration.rst +++ b/user_guide_src/source/database/configuration.rst @@ -150,7 +150,7 @@ Explanation of Values: See `SQLite documentation `_. To enforce Foreign Key constraint, set this config item to true. **busyTimeout** milliseconds (int) - Sleeps for a specified amount of time when a table is locked (``SQLite3`` only). -**numberNative** milliseconds (int) - Sleeps for a specified amount of time when a table is locked (``SQLite3`` only). +**numberNative** true/false (boolean) - Whether or not to enable MYSQLI_OPT_INT_AND_FLOAT_NATIVE (``MySQLi`` only). ================ =========================================================================================================== .. note:: Depending on what database driver you are using (``MySQLi``, ``Postgres``, From c18581dd0eec72cd018a68abc6f5a72cf0cd0a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Sun, 19 Feb 2023 13:37:01 +0800 Subject: [PATCH 062/485] fix:Run composer cs-fix and modify the Enhancements/DataBase description in user_guide_src/source/changelogs/v4.4.0.rst. --- system/Database/MySQLi/Connection.php | 6 +++--- user_guide_src/source/changelogs/v4.4.0.rst | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 00c165dbef51..45b306ce2a44 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -105,9 +105,9 @@ public function connect(bool $persistent = false) mysqli_report(MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX); $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10); - - if($this->numberNative === true){ - $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE,1); + + if ($this->numberNative === true) { + $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1); } if (isset($this->strictOn)) { diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index a4d2afbafec2..8844656b91dd 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -59,6 +59,9 @@ Forge Others ------ +- **numberNative:** Added the ``numberNative`` attribute to the Database setting to keep the variable type obtained after SQL Query consistent with the type set in the DataBase. + See `configuration.rst`_ for details. + Model ===== From b2c74e8b7d3d6425abb918747a0cad0191a2e871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Sun, 19 Feb 2023 13:48:48 +0800 Subject: [PATCH 063/485] fix:Modify the Enhancements/DataBase description in user_guide_src/source/changelogs/v4.4.0.rst. --- user_guide_src/source/changelogs/v4.4.0.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 8844656b91dd..959dc75bdd87 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -59,8 +59,9 @@ Forge Others ------ -- **numberNative:** Added the ``numberNative`` attribute to the Database setting to keep the variable type obtained after SQL Query consistent with the type set in the DataBase. - See `configuration.rst`_ for details. +- **Database:** Added the ``numberNative`` attribute to the Database Config for MySQLi to keep the variable type obtained after SQL Query consistent with the type set in the database. + See `configuration.rst `_ + for details. Model ===== From 78ff0a9775f19a0ed0af84209ae3d295c4c9b205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Sun, 19 Feb 2023 14:19:49 +0800 Subject: [PATCH 064/485] fix:Modify database/configuration.rst and changelogs/v4.4.0.rst description. --- user_guide_src/source/changelogs/v4.4.0.rst | 3 +-- user_guide_src/source/database/configuration.rst | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 959dc75bdd87..8d3cc6894dfc 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -60,8 +60,7 @@ Others ------ - **Database:** Added the ``numberNative`` attribute to the Database Config for MySQLi to keep the variable type obtained after SQL Query consistent with the type set in the database. - See `configuration.rst `_ - for details. + See :ref:`Database Configuration `. Model ===== diff --git a/user_guide_src/source/database/configuration.rst b/user_guide_src/source/database/configuration.rst index 1683ce801518..cfd666988ac3 100644 --- a/user_guide_src/source/database/configuration.rst +++ b/user_guide_src/source/database/configuration.rst @@ -104,6 +104,8 @@ default group's configuration settings. The values should be name following this database.default.password = ''; database.default.database = 'ci4'; +.. _database-configuration-explanation-of-values: + ********************** Explanation of Values: ********************** From 0a8c9711597c7bc88de429e72b30a5285ecbc4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E9=8A=98=E5=87=B1?= Date: Sun, 19 Feb 2023 14:41:18 +0800 Subject: [PATCH 065/485] fix:Modify changelogs/v4.4.0.rst description and remove the numberNative attribute from the $test array in app/Config/Database.php --- app/Config/Database.php | 39 ++++++++++----------- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/app/Config/Database.php b/app/Config/Database.php index 9e752027feed..e2450ec16cf1 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -50,26 +50,25 @@ class Database extends Config * running PHPUnit database tests. */ public array $tests = [ - 'DSN' => '', - 'hostname' => '127.0.0.1', - 'username' => '', - 'password' => '', - 'database' => ':memory:', - 'DBDriver' => 'SQLite3', - 'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS - 'pConnect' => false, - 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', - 'swapPre' => '', - 'encrypt' => false, - 'compress' => false, - 'strictOn' => false, - 'failover' => [], - 'port' => 3306, - 'foreignKeys' => true, - 'busyTimeout' => 1000, - 'numberNative' => false, + 'DSN' => '', + 'hostname' => '127.0.0.1', + 'username' => '', + 'password' => '', + 'database' => ':memory:', + 'DBDriver' => 'SQLite3', + 'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS + 'pConnect' => false, + 'DBDebug' => true, + 'charset' => 'utf8', + 'DBCollat' => 'utf8_general_ci', + 'swapPre' => '', + 'encrypt' => false, + 'compress' => false, + 'strictOn' => false, + 'failover' => [], + 'port' => 3306, + 'foreignKeys' => true, + 'busyTimeout' => 1000, ]; public function __construct() diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 8d3cc6894dfc..24ef1efc1707 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -59,7 +59,7 @@ Forge Others ------ -- **Database:** Added the ``numberNative`` attribute to the Database Config for MySQLi to keep the variable type obtained after SQL Query consistent with the type set in the database. +- **MySQLi:** Added the ``numberNative`` attribute to the Database Config to keep the variable type obtained after SQL Query consistent with the type set in the database. See :ref:`Database Configuration `. Model From 2c815c64ed1e2b591d049d867fb34a7cc9695abb Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 7 Feb 2023 19:05:40 +0900 Subject: [PATCH 066/485] feat: add new setter/getter method for Entity --- system/Entity/Entity.php | 18 +++++++-- tests/system/Entity/EntityTest.php | 59 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 5220ae5b0aa8..ffb693cae467 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -456,11 +456,19 @@ public function __set(string $key, $value = null) $value = $this->castAs($value, $key, 'set'); - // if a set* method exists for this key, use that method to + // if a setter method exists for this key, use that method to // insert this value. should be outside $isNullable check, // so maybe wants to do sth with null value automatically $method = 'set' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key))); + // If a "`_set` + $key" method exists, it is a setter. + if (method_exists($this, '_' . $method)) { + $this->{'_' . $method}($value); + + return $this; + } + + // If a "`set` + $key" method exists, it is also a setter. if (method_exists($this, $method) && $method !== 'setAttributes') { $this->{$method}($value); @@ -499,9 +507,13 @@ public function __get(string $key) // Convert to CamelCase for the method $method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key))); - // if a get* method exists for this key, + // if a getter method exists for this key, // use that method to insert this value. - if (method_exists($this, $method)) { + if (method_exists($this, '_' . $method)) { + // If a "`_get` + $key" method exists, it is a getter. + $result = $this->{'_' . $method}(); + } elseif (method_exists($this, $method)) { + // If a "`get` + $key" method exists, it is also a getter. $result = $this->{$method}(); } diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 79aeb53f3199..41192e3f9805 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -82,6 +82,19 @@ public function testGetterSetters() $this->assertSame('bar:thanks:bar', $entity->bar); } + public function testNewGetterSetters() + { + $entity = $this->getNewSetterGetterEntity(); + + $entity->bar = 'thanks'; + + $this->assertSame('bar:thanks:bar', $entity->bar); + + $entity->setBar('BAR'); + + $this->assertSame('BAR', $entity->getBar()); + } + public function testUnsetUnsetsAttribute() { $entity = $this->getEntity(); @@ -1096,6 +1109,52 @@ public function getFakeBar() }; } + protected function getNewSetterGetterEntity() + { + return new class () extends Entity { + protected $attributes = [ + 'foo' => null, + 'bar' => null, + 'default' => 'sumfin', + 'created_at' => null, + ]; + protected $original = [ + 'foo' => null, + 'bar' => null, + 'default' => 'sumfin', + 'created_at' => null, + ]; + protected $datamap = [ + 'createdAt' => 'created_at', + ]; + private string $bar; + + public function setBar($value) + { + $this->bar = $value; + + return $this; + } + + public function getBar() + { + return $this->bar; + } + + public function _setBar($value) + { + $this->attributes['bar'] = "bar:{$value}"; + + return $this; + } + + public function _getBar() + { + return "{$this->attributes['bar']}:bar"; + } + }; + } + protected function getMappedEntity() { return new class () extends Entity { From 6f52f1e5169d54adc5887528786ad46b9fccc39b Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 20 Feb 2023 10:49:41 +0900 Subject: [PATCH 067/485] docs: add user guide --- user_guide_src/source/changelogs/v4.4.0.rst | 3 +++ user_guide_src/source/models/entities.rst | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index a4d2afbafec2..48d48d6d8c73 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -62,6 +62,9 @@ Others Model ===== +- Added special getter/setter to Entity to avoid method name conflicts. + See :ref:`entities-special-getter-setter`. + Libraries ========= diff --git a/user_guide_src/source/models/entities.rst b/user_guide_src/source/models/entities.rst index df625dd73833..b93777ea125e 100644 --- a/user_guide_src/source/models/entities.rst +++ b/user_guide_src/source/models/entities.rst @@ -131,6 +131,25 @@ business logic and create objects that are pleasant to use. .. literalinclude:: entities/007.php +.. _entities-special-getter-setter: + +Special Getter/Setter +--------------------- + +.. versionadded:: 4.4.0 + +For example, if your Entity's parent class already has a ``getParent()`` method +defined, and your Entity also has a column named ``parent``, when you try to add +business logic to the ``getParent()`` method in your Entity class, the method is +already defined. + +In such a case, you can use the special getter/setter. Instead of ``getX()``/``setX()``, +set ``_getX()``/``_setX()``. + +In the above example, if your Entity has the ``_getParent()`` method, the method +will be used when you get ``$entity->parent``, and the ``_setParent()`` method +will be used when you set ``$entity->parent``. + Data Mapping ============ From 7cc02e824669c8adaaa7225f63e84623f861b3d7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Feb 2023 15:43:08 +0900 Subject: [PATCH 068/485] docs: add PHPDoc --- system/CodeIgniter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index d370e34ca22e..85e43e4c2a1b 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -122,7 +122,7 @@ class CodeIgniter /** * Cache expiration time * - * @var int + * @var int seconds */ protected static $cacheTTL = 0; From 2e80f8364c26310e105f31d1fb0add9656e52bcd Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Feb 2023 15:44:20 +0900 Subject: [PATCH 069/485] refactor: use config() instead of new keyword --- system/CodeIgniter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 85e43e4c2a1b..544fcb2b813e 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -351,7 +351,7 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon // Check for a cached page. Execution will stop // if the page has been cached. - $cacheConfig = new Cache(); + $cacheConfig = config(Cache::class); $response = $this->displayCache($cacheConfig); if ($response instanceof ResponseInterface) { if ($returnResponse) { From a17a2a09c8a1fd835c87848fa065b9577e716e7c Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Feb 2023 15:45:31 +0900 Subject: [PATCH 070/485] test: change to something more realistic A CodeIgniter instance handle one request. Use page caching in the run() method, instead of forcing to call cachePage(). Do not register invalid routes with query string. --- tests/system/CodeIgniterTest.php | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 4c6ec5ce8689..f5c1b3e8cfc5 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -761,7 +761,7 @@ public function testPageCacheWithCacheQueryString( CITestStreamFilter::addErrorFilter(); // Create cache config with cacheQueryString value from the dataProvider - $cacheConfig = new Cache(); + $cacheConfig = config(Cache::class); $cacheConfig->cacheQueryString = $cacheQueryStringValue; // Clear cache before starting the test @@ -773,15 +773,16 @@ public function testPageCacheWithCacheQueryString( // Generate request to each URL from the testing array foreach ($testingUrls as $testingUrl) { + $this->resetServices(); $_SERVER['REQUEST_URI'] = '/' . $testingUrl; - $routes = Services::routes(true); - $routes->add($testingUrl, static function () { - // Don't cache the page in the run() function because CodeIgniter - // class will create default $cacheConfig and overwrite settings - // from the dataProvider - CodeIgniter::cache(0); + $this->codeigniter = new MockCodeIgniter(new App()); + + $routes = Services::routes(true); + $routePath = explode('?', $testingUrl)[0]; + $string = 'This is a test page, to check cache configuration'; + $routes->add($routePath, static function () use ($string) { + CodeIgniter::cache(60); $response = Services::response(); - $string = 'This is a test page, to check cache configuration'; return $response->setBody($string); }); @@ -792,9 +793,15 @@ public function testPageCacheWithCacheQueryString( // Cache the page output using default caching function and $cacheConfig // with value from the data provider + ob_start(); $this->codeigniter->run(); - // Cache the page using our own $cacheConfig confugration - $this->codeigniter->cachePage($cacheConfig); + $output = ob_get_clean(); + + $this->assertSame($string, $output); + + if (count(ob_list_handlers()) > 1) { + ob_end_clean(); + } } // Calculate how much cached items exist in the cache after the test requests From 08493fb33f7b9ed01c917b6de4538762a9c77bf4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Feb 2023 15:50:53 +0900 Subject: [PATCH 071/485] docs: fix PHPDoc types --- system/CodeIgniter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 544fcb2b813e..2ad67b50e4a6 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -734,7 +734,7 @@ public static function cache(int $time) * Caches the full response from the current request. Used for * full-page caching for very high performance. * - * @return mixed + * @return bool */ public function cachePage(Cache $config) { @@ -920,7 +920,7 @@ protected function createController() * 2. PHP CLI: accessed by CLI via php public/index.php, arguments become URI segments, * sent to Controllers via Routes, output varies * - * @param mixed $class + * @param Controller $class * * @return false|ResponseInterface|string|void */ From 1dfa4fefc65b67319bff33f6eecb2f7ae2841788 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 08:48:46 +0900 Subject: [PATCH 072/485] refactor: use config() in display404errors() --- system/CodeIgniter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 2ad67b50e4a6..32db30f90591 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -965,7 +965,7 @@ protected function display404errors(PageNotFoundException $e) unset($override); - $cacheConfig = new Cache(); + $cacheConfig = config(Cache::class); $this->gatherOutput($cacheConfig, $returned); if ($this->returnResponse) { return $this->response; From 7410504f9e370f4c3d42394b49b685ab915830dc Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 17:27:11 +0900 Subject: [PATCH 073/485] docs: fix conflict --- user_guide_src/source/incoming/filters.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/user_guide_src/source/incoming/filters.rst b/user_guide_src/source/incoming/filters.rst index b6082522cf6e..6c6b525ce838 100644 --- a/user_guide_src/source/incoming/filters.rst +++ b/user_guide_src/source/incoming/filters.rst @@ -161,12 +161,10 @@ a list of URI patterns that filter should apply to: .. literalinclude:: filters/009.php -<<<<<<< HEAD .. _filters-filters-filter-arguments: -======= + Filter Arguments ================ ->>>>>>> upstream/develop Filter Arguments ---------------- From a54ee527e1f4d369ee8c1e2ec65b697cc1bef2a9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 21:51:26 +0900 Subject: [PATCH 074/485] test: use ob_get_level() --- tests/system/CodeIgniterTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index f5c1b3e8cfc5..a7f5b9bd1908 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -53,7 +53,7 @@ protected function tearDown(): void { parent::tearDown(); - if (count(ob_list_handlers()) > 1) { + if (ob_get_level() > 1) { ob_end_clean(); } @@ -799,7 +799,7 @@ public function testPageCacheWithCacheQueryString( $this->assertSame($string, $output); - if (count(ob_list_handlers()) > 1) { + if (ob_get_level() > 1) { ob_end_clean(); } } From 4726bd50deb4c7ea8514321896f2996913f83fa2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 15:36:44 +0900 Subject: [PATCH 075/485] feat: SQLSRV getFieldData() supports nullable --- system/Database/SQLSRV/Connection.php | 10 +++++++--- tests/system/Database/Live/ForgeTest.php | 15 ++++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 5d9b208d6010..97f876b2d9d0 100755 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -341,9 +341,11 @@ protected function _enableForeignKeyChecks() */ protected function _fieldData(string $table): array { - $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_NAME= ' . $this->escape(($table)); + $sql = 'SELECT + COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, + COLUMN_DEFAULT, IS_NULLABLE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME= ' . $this->escape(($table)); if (($query = $this->query($sql)) === false) { throw new DatabaseException(lang('Database.failGetFieldData')); @@ -362,6 +364,8 @@ protected function _fieldData(string $table): array $retVal[$i]->max_length = $query[$i]->CHARACTER_MAXIMUM_LENGTH > 0 ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION; + + $retVal[$i]->nullable = $query[$i]->IS_NULLABLE !== 'NO'; } return $retVal; diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index d859bc93833c..44fe174fdf1e 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -848,6 +848,7 @@ public function testAddFields() 'name' => [ 'type' => 'VARCHAR', 'constraint' => 255, + 'null' => true, ], 'active' => [ 'type' => 'INTEGER', @@ -889,7 +890,7 @@ public function testAddFields() 'name' => 'name', 'type' => 'varchar', 'max_length' => 255, - 'nullable' => false, + 'nullable' => true, 'default' => null, 'primary_key' => 0, ], @@ -929,7 +930,7 @@ public function testAddFields() 2 => [ 'name' => 'name', 'type' => 'character varying', - 'nullable' => false, + 'nullable' => true, 'default' => null, 'max_length' => '255', ], @@ -965,7 +966,7 @@ public function testAddFields() 'max_length' => null, 'default' => null, 'primary_key' => false, - 'nullable' => false, + 'nullable' => true, ], 3 => [ 'name' => 'active', @@ -983,24 +984,28 @@ public function testAddFields() 'type' => 'int', 'default' => null, 'max_length' => 10, + 'nullable' => false, ], 1 => [ 'name' => 'username', 'type' => 'varchar', 'default' => null, 'max_length' => 255, + 'nullable' => false, ], 2 => [ 'name' => 'name', 'type' => 'varchar', 'default' => null, 'max_length' => 255, + 'nullable' => true, ], 3 => [ 'name' => 'active', 'type' => 'int', 'default' => '((0))', // Why? 'max_length' => 10, + 'nullable' => false, ], ]; } elseif ($this->db->DBDriver === 'OCI8') { @@ -1023,8 +1028,8 @@ public function testAddFields() 'name' => 'name', 'type' => 'VARCHAR2', 'max_length' => '255', - 'default' => '', - 'nullable' => false, + 'default' => null, + 'nullable' => true, ], 3 => [ 'name' => 'active', From a98658ba820f68fa6e070e4e6f57d63b2b415f7e Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 17:22:03 +0900 Subject: [PATCH 076/485] docs: add user guide --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + user_guide_src/source/database/metadata.rst | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 24ef1efc1707..6cbf8e80952f 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -61,6 +61,7 @@ Others - **MySQLi:** Added the ``numberNative`` attribute to the Database Config to keep the variable type obtained after SQL Query consistent with the type set in the database. See :ref:`Database Configuration `. +- **SQLSRV:** Field Metadata now includes ``nullable``. See :ref:`db-metadata-getfielddata`. Model ===== diff --git a/user_guide_src/source/database/metadata.rst b/user_guide_src/source/database/metadata.rst index 101d0a605683..607a1025be8b 100644 --- a/user_guide_src/source/database/metadata.rst +++ b/user_guide_src/source/database/metadata.rst @@ -78,6 +78,8 @@ performing an action. Returns a boolean true/false. Usage example: Retrieve Field Metadata ======================= +.. _db-metadata-getfielddata: + $db->getFieldData() ------------------- @@ -104,9 +106,11 @@ database: - type - the type of the column - max_length - maximum length of the column - primary_key - integer ``1`` if the column is a primary key (all integer ``1``, even if there are multiple primary keys), otherwise integer ``0`` (This field is currently only available for MySQL and SQLite3) -- nullable - boolean ``true`` if the column is nullable, otherwise boolean ``false`` (This field is currently not available in SQL Server) +- nullable - boolean ``true`` if the column is nullable, otherwise boolean ``false`` - default - the default value +.. note:: Since v4.4.0, SQLSRV supported ``nullable``. + List the Indexes in a Table =========================== From 69ed98db7cf55bb4e118986750f700e3a581a3e0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 27 Feb 2023 10:43:13 +0900 Subject: [PATCH 077/485] refactor: deprecate Autoloader::sanitizeFilename() --- system/Autoloader/Autoloader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index f838451ce6fb..f4251cf06537 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -306,8 +306,6 @@ protected function loadInNamespace(string $class) */ protected function includeFile(string $file) { - $file = $this->sanitizeFilename($file); - if (is_file($file)) { include_once $file; @@ -327,6 +325,8 @@ protected function includeFile(string $file) * and end of filename. * * @return string The sanitized filename + * + * @deprecated No longer used. See https://github.com/codeigniter4/CodeIgniter4/issues/7055 */ public function sanitizeFilename(string $filename): string { From 5ca1a9540e2fb700986afbcc54a5f3bcba60668e Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 27 Feb 2023 10:49:15 +0900 Subject: [PATCH 078/485] docs: update user guide --- user_guide_src/source/installation/installing_composer.rst | 7 +++++-- user_guide_src/source/installation/installing_manual.rst | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/installation/installing_composer.rst b/user_guide_src/source/installation/installing_composer.rst index ca9eacf28314..1bd6194e2fad 100644 --- a/user_guide_src/source/installation/installing_composer.rst +++ b/user_guide_src/source/installation/installing_composer.rst @@ -44,9 +44,12 @@ The command above will create a **project-root** folder. If you omit the "project-root" argument, the command will create an "appstarter" folder, which can be renamed as appropriate. -.. note:: CodeIgniter autoloader does not allow special characters that are illegal in filenames on certain operating systems. +.. note:: Before v4.4.0, CodeIgniter autoloader did not allow special + characters that are illegal in filenames on certain operating systems. The symbols that can be used are ``/``, ``_``, ``.``, ``:``, ``\`` and space. - So if you install CodeIgniter under the folder that contains the special characters like ``(``, ``)``, etc., CodeIgniter won't work. + So if you installed CodeIgniter under the folder that contains the special + characters like ``(``, ``)``, etc., CodeIgniter didn't work. Since v4.4.0, + this restriction has been removed. .. important:: When you deploy to your production server, don't forget to run the following command:: diff --git a/user_guide_src/source/installation/installing_manual.rst b/user_guide_src/source/installation/installing_manual.rst index 7e592046f3a5..8b55e375de0b 100644 --- a/user_guide_src/source/installation/installing_manual.rst +++ b/user_guide_src/source/installation/installing_manual.rst @@ -22,9 +22,12 @@ Installation Download the `latest version `_, and extract it to become your project root. -.. note:: CodeIgniter autoloader does not allow special characters that are illegal in filenames on certain operating systems. +.. note:: Before v4.4.0, CodeIgniter autoloader did not allow special + characters that are illegal in filenames on certain operating systems. The symbols that can be used are ``/``, ``_``, ``.``, ``:``, ``\`` and space. - So if you install CodeIgniter under the folder that contains the special characters like ``(``, ``)``, etc., CodeIgniter won't work. + So if you installed CodeIgniter under the folder that contains the special + characters like ``(``, ``)``, etc., CodeIgniter didn't work. Since v4.4.0, + this restriction has been removed. Initial Configuration ===================== From dab1ae67542910764b91a2beac0f9279524ad5fb Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 27 Feb 2023 10:52:06 +0900 Subject: [PATCH 079/485] docs: add changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 19663e6b524d..5f902d226f85 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -94,11 +94,18 @@ Changes - **Config:** The deprecated Cookie items in **app/Config/App.php** has been removed. - **DownloadResponse:** When generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. +- **Autoloader:** Before v4.4.0, CodeIgniter autoloader did not allow special + characters that are illegal in filenames on certain operating systems. + The symbols that can be used are ``/``, ``_``, ``.``, ``:``, ``\`` and space. + So if you installed CodeIgniter under the folder that contains the special + characters like ``(``, ``)``, etc., CodeIgniter didn't work. Since v4.4.0, + this restriction has been removed. Deprecations ************ - **Entity:** ``Entity::setAttributes()`` is deprecated. Use ``Entity::injectRawData()`` instead. +- **Autoloader:** ``Autoloader::sanitizeFilename()`` is deprecated. Bugs Fixed ********** From f65409b33ae74bad3c06f619b7b96296335836e7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 27 Feb 2023 10:21:40 +0900 Subject: [PATCH 080/485] feat: add setValidLocales() --- system/HTTP/IncomingRequest.php | 12 ++++++++++++ tests/system/HTTP/IncomingRequestTest.php | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 008a865bcdcd..8012df3eb8e3 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -548,6 +548,18 @@ public function setLocale(string $locale) return $this; } + /** + * Set the valid locales. + * + * @return $this + */ + public function setValidLocales(array $locales) + { + $this->validLocales = $locales; + + return $this; + } + /** * Gets the current locale, with a fallback to the default * locale if none is set. diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index f6994f722ce9..2b61850ec579 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -205,6 +205,21 @@ public function testSetBadLocale() $this->assertSame('es', $request->getLocale()); } + public function testSetValidLocales() + { + $config = new App(); + $config->supportedLocales = ['en', 'es']; + $config->defaultLocale = 'es'; + $config->baseURL = 'http://example.com/'; + + $request = new IncomingRequest($config, new URI(), null, new UserAgent()); + + $request->setValidLocales(['ja']); + $request->setLocale('ja'); + + $this->assertSame('ja', $request->getLocale()); + } + /** * @see https://github.com/codeigniter4/CodeIgniter4/issues/2774 */ From 3b3a5e8f5ed61bde026267bf605df7ffc8c74f76 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Mar 2023 11:43:13 +0900 Subject: [PATCH 081/485] docs: add docs --- user_guide_src/source/outgoing/localization.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index 2cff7700b091..86769f009f80 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -54,6 +54,9 @@ will be used to set the locale. Should you ever need to set the locale directly you may use ``IncomingRequest::setLocale(string $locale)``. +Since v4.4.0, ``IncomingRequest::setValidLocales()`` has been added to set +(and reset) valid locales that are set from ``Config\App::$supportedLocales`` setting. + Content Negotiation ------------------- From 561e2497035c8a0b93a71bb720aed0cc494b9004 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Mar 2023 11:43:26 +0900 Subject: [PATCH 082/485] docs: add changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 5f902d226f85..1781eaaff0d8 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -85,6 +85,7 @@ Others ``product/15`` where ``15`` is an arbitrary number. See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. +- **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. Message Changes *************** From 970d7ace1f74a29f26af2867f186c4ca6002b930 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Mar 2023 10:04:34 +0900 Subject: [PATCH 083/485] refactor: improve error message for HTTP.invalidHTTPProtocol Valid versions are obvious, so not needed. --- system/HTTP/Exceptions/HTTPException.php | 4 ++-- system/HTTP/MessageTrait.php | 2 +- system/Language/en/HTTP.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/system/HTTP/Exceptions/HTTPException.php b/system/HTTP/Exceptions/HTTPException.php index 966c3c7fd7e5..d22c48bcb9f3 100644 --- a/system/HTTP/Exceptions/HTTPException.php +++ b/system/HTTP/Exceptions/HTTPException.php @@ -77,9 +77,9 @@ public static function forInvalidNegotiationType(string $type) * * @return HTTPException */ - public static function forInvalidHTTPProtocol(string $protocols) + public static function forInvalidHTTPProtocol(string $invalidVersion) { - return new static(lang('HTTP.invalidHTTPProtocol', [$protocols])); + return new static(lang('HTTP.invalidHTTPProtocol', [$invalidVersion])); } /** diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index 7e2665d09157..7482f5f59681 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -229,7 +229,7 @@ public function setProtocolVersion(string $version): self $version = number_format((float) $version, 1); if (! in_array($version, $this->validProtocolVersions, true)) { - throw HTTPException::forInvalidHTTPProtocol(implode(', ', $this->validProtocolVersions)); + throw HTTPException::forInvalidHTTPProtocol($version); } $this->protocolVersion = $version; diff --git a/system/Language/en/HTTP.php b/system/Language/en/HTTP.php index 942e3cf9dd2a..a7861c7188d1 100644 --- a/system/Language/en/HTTP.php +++ b/system/Language/en/HTTP.php @@ -21,7 +21,7 @@ 'invalidNegotiationType' => '"{0}" is not a valid negotiation type. Must be one of: media, charset, encoding, language.', // Message - 'invalidHTTPProtocol' => 'Invalid HTTP Protocol Version. Must be one of: {0}', + 'invalidHTTPProtocol' => 'Invalid HTTP Protocol Version: {0}', // Negotiate 'emptySupportedNegotiations' => 'You must provide an array of supported values to all Negotiations.', From 517eadeda92efac9d6e07e7641b2c0c625cc10f4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Mar 2023 10:11:12 +0900 Subject: [PATCH 084/485] docs: add changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 7b394d66ac15..503922b418e2 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -91,6 +91,8 @@ Others Message Changes *************** +- Improved ``HTTP.invalidHTTPProtocol`` error message. + Changes ******* From c7b02d89eb573cf561ff73ff93d6c34f7e947298 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Tue, 4 Apr 2023 00:33:15 +0200 Subject: [PATCH 085/485] data row columns optional order by heading keys --- system/View/Table.php | 48 +++++++++++++++++++++++++++-- tests/system/View/TableTest.php | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/system/View/Table.php b/system/View/Table.php index a7ded4fd5cb7..a7a730b667dc 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -83,6 +83,13 @@ class Table */ public $function; + /** + * Order each inserted row by heading keys + * + * @var bool + */ + public bool $rowKeysSyncWithHeadingKeys = false; + /** * Set the template from the table config file if it exists * @@ -162,6 +169,7 @@ public function makeColumns($array = [], $columnLimit = 0) // Turn off the auto-heading feature since it's doubtful we // will want headings from a one-dimensional array $this->autoHeading = false; + $this->rowKeysSyncWithHeadingKeys = false; if ($columnLimit === 0) { return $array; @@ -207,7 +215,42 @@ public function setEmpty($value) */ public function addRow() { - $this->rows[] = $this->_prepArgs(func_get_args()); + $tmpRow = $this->_prepArgs(func_get_args()); + + if ($this->rowKeysSyncWithHeadingKeys && !empty($this->heading)) { + // each key has an index + $keyIndex = array_flip(array_keys($this->heading)); + + // figure out which keys need to be added + $missingKeys = array_diff_key($keyIndex, $tmpRow); + + // Remove all keys which don't exist in $keyIndex + $tmpRow = array_filter($tmpRow, static fn($k) => array_key_exists($k, $keyIndex), ARRAY_FILTER_USE_KEY); + + // add missing keys to row, but use $this->emptyCells + $tmpRow = array_merge($tmpRow, array_map(fn($v) => ['data' => $this->emptyCells], $missingKeys)); + + // order keys by $keyIndex values + uksort($tmpRow, static fn($k1, $k2) => $keyIndex[$k1] <=> $keyIndex[$k2]); + } + $this->rows[] = $tmpRow; + + return $this; + } + + /** + * Set to true if each row column should be synced by keys defined in heading. + * + * If a row has a key which does not exist in heading, it will be filtered out + * If a row does not have a key which exists in heading, the field will stay empty + * + * @param bool $orderByKey + * + * @return Table + */ + public function setSyncRowKeysWithHeadingKeys(bool $orderByKey): Table + { + $this->rowKeysSyncWithHeadingKeys = $orderByKey; return $this; } @@ -436,7 +479,8 @@ protected function _setFromArray($data) } foreach ($data as &$row) { - $this->rows[] = $this->_prepArgs($row); + $this->addRow($row); + //$this->rows[] = $this->_prepArgs($row); } } diff --git a/tests/system/View/TableTest.php b/tests/system/View/TableTest.php index 6e56d2a3fe22..71481ea73717 100644 --- a/tests/system/View/TableTest.php +++ b/tests/system/View/TableTest.php @@ -763,6 +763,59 @@ public function testInvalidCallback() $this->assertStringContainsString('FredBlueSmall', $generated); } + + /** + * @dataProvider orderedColumnUsecases + */ + public function testAddRowAndGenerateWithOrderedColumns(array $heading, array $row, string $expectContainsString): void + { + $this->table->setHeading($heading); + $this->table->setSyncRowKeysWithHeadingKeys(true); + $this->table->addRow($row); + + $generated = $this->table->generate(); + + $this->assertStringContainsString($expectContainsString, $generated); + } + + /** + * @dataProvider orderedColumnUsecases + */ + public function testGenerateDataWithOrderedColumns(array $heading, array $row, string $expectContainsString): void + { + $this->table->setHeading($heading); + $this->table->setSyncRowKeysWithHeadingKeys(true); + + $generated = $this->table->generate([$row]); + + $this->assertStringContainsString($expectContainsString, $generated); + } + + public function orderedColumnUsecases(): array + { + return [ + [ + 'heading' => ['id' => 'ID', 'name' => 'Name', 'age' => 'Age'], + 'row' => ['name' => 'Max', 'age' => 30, 'id' => 5], + 'expectContainsString' => '5Max30' + ], + [ + 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'], + 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5], + 'expectContainsString' => '530Fred' + ], + [ + 'heading' => ['id' => 'ID', 'name' => 'Name'], + 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5], + 'expectContainsString' => '5Fred' + ], + [ + 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'], + 'row' => ['name' => 'Fred', 'id' => 5], + 'expectContainsString' => '5Fred' + ] + ]; + } } // We need this for the _set_from_db_result() test From f420ad98ae6aad357fe91fa86fc6a04e2fdf26e7 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Tue, 4 Apr 2023 20:36:19 +0200 Subject: [PATCH 086/485] Docu, CS fixes --- system/View/Table.php | 19 +++------- tests/system/View/TableTest.php | 24 ++++++------ user_guide_src/source/outgoing/table.rst | 26 +++++++++++++ user_guide_src/source/outgoing/table/019.php | 40 ++++++++++++++++++++ 4 files changed, 84 insertions(+), 25 deletions(-) create mode 100644 user_guide_src/source/outgoing/table/019.php diff --git a/system/View/Table.php b/system/View/Table.php index a7a730b667dc..94373f360d0e 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -85,8 +85,6 @@ class Table /** * Order each inserted row by heading keys - * - * @var bool */ public bool $rowKeysSyncWithHeadingKeys = false; @@ -168,7 +166,7 @@ public function makeColumns($array = [], $columnLimit = 0) // Turn off the auto-heading feature since it's doubtful we // will want headings from a one-dimensional array - $this->autoHeading = false; + $this->autoHeading = false; $this->rowKeysSyncWithHeadingKeys = false; if ($columnLimit === 0) { @@ -217,7 +215,7 @@ public function addRow() { $tmpRow = $this->_prepArgs(func_get_args()); - if ($this->rowKeysSyncWithHeadingKeys && !empty($this->heading)) { + if ($this->rowKeysSyncWithHeadingKeys && ! empty($this->heading)) { // each key has an index $keyIndex = array_flip(array_keys($this->heading)); @@ -225,13 +223,13 @@ public function addRow() $missingKeys = array_diff_key($keyIndex, $tmpRow); // Remove all keys which don't exist in $keyIndex - $tmpRow = array_filter($tmpRow, static fn($k) => array_key_exists($k, $keyIndex), ARRAY_FILTER_USE_KEY); + $tmpRow = array_filter($tmpRow, static fn ($k) => array_key_exists($k, $keyIndex), ARRAY_FILTER_USE_KEY); // add missing keys to row, but use $this->emptyCells - $tmpRow = array_merge($tmpRow, array_map(fn($v) => ['data' => $this->emptyCells], $missingKeys)); + $tmpRow = array_merge($tmpRow, array_map(fn ($v) => ['data' => $this->emptyCells], $missingKeys)); // order keys by $keyIndex values - uksort($tmpRow, static fn($k1, $k2) => $keyIndex[$k1] <=> $keyIndex[$k2]); + uksort($tmpRow, static fn ($k1, $k2) => $keyIndex[$k1] <=> $keyIndex[$k2]); } $this->rows[] = $tmpRow; @@ -240,13 +238,9 @@ public function addRow() /** * Set to true if each row column should be synced by keys defined in heading. - * + * * If a row has a key which does not exist in heading, it will be filtered out * If a row does not have a key which exists in heading, the field will stay empty - * - * @param bool $orderByKey - * - * @return Table */ public function setSyncRowKeysWithHeadingKeys(bool $orderByKey): Table { @@ -480,7 +474,6 @@ protected function _setFromArray($data) foreach ($data as &$row) { $this->addRow($row); - //$this->rows[] = $this->_prepArgs($row); } } diff --git a/tests/system/View/TableTest.php b/tests/system/View/TableTest.php index 71481ea73717..da567b63dbd6 100644 --- a/tests/system/View/TableTest.php +++ b/tests/system/View/TableTest.php @@ -794,24 +794,24 @@ public function testGenerateDataWithOrderedColumns(array $heading, array $row, s public function orderedColumnUsecases(): array { return [ - [ - 'heading' => ['id' => 'ID', 'name' => 'Name', 'age' => 'Age'], - 'row' => ['name' => 'Max', 'age' => 30, 'id' => 5], + 'reorder example #1' => [ + 'heading' => ['id' => 'ID', 'name' => 'Name', 'age' => 'Age'], + 'row' => ['name' => 'Max', 'age' => 30, 'id' => 5], 'expectContainsString' => '5Max30' ], - [ - 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'], - 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5], + 'reorder example #2' => [ + 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'], + 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5], 'expectContainsString' => '530Fred' ], - [ - 'heading' => ['id' => 'ID', 'name' => 'Name'], - 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5], + '2 col heading, 3 col data row' => [ + 'heading' => ['id' => 'ID', 'name' => 'Name'], + 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5], 'expectContainsString' => '5Fred' ], - [ - 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'], - 'row' => ['name' => 'Fred', 'id' => 5], + '3 col heading, 2 col data row' => [ + 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'], + 'row' => ['name' => 'Fred', 'id' => 5], 'expectContainsString' => '5Fred' ] ]; diff --git a/user_guide_src/source/outgoing/table.rst b/user_guide_src/source/outgoing/table.rst index 0ccf8da0904d..8b876ac37e85 100644 --- a/user_guide_src/source/outgoing/table.rst +++ b/user_guide_src/source/outgoing/table.rst @@ -71,6 +71,23 @@ to the Table constructor: .. literalinclude:: table/008.php +Synchronizing row columns with heading +====================================== + +.. versionadded:: 4.4.0 + +``setSyncRowKeysWithHeadingKeys(true)`` enables that each data value +is placed in the same column as defined in ``setHeading()`` if an +associative array was used as parameter. This is especially useful +when dealing with data loaded via REST API where the order is not to +your liking, or if the API returned too much data. + +If a data row contains a ``key`` which does not exist the heading, +the value will be filtered out. Vise versa if a data row does not have key +mentioned in heading, it places an empty cell for that spot. + +.. literalinclude:: table/019.php + *************** Class Reference *************** @@ -188,3 +205,12 @@ Class Reference Example .. literalinclude:: table/018.php + + .. php:method:: setSyncRowKeysWithHeadingKeys(bool $orderByKey) + + :returns: Table instance (method chaining) + :rtype: Table + + Enables each row column to be ordered by heading keys. This gives + more control of how data is displayed in the final table. Make + sure to set this value before calling the first ``addRow()`` method. diff --git a/user_guide_src/source/outgoing/table/019.php b/user_guide_src/source/outgoing/table/019.php new file mode 100644 index 000000000000..2edcac9c0197 --- /dev/null +++ b/user_guide_src/source/outgoing/table/019.php @@ -0,0 +1,40 @@ +setHeading(['name' => 'Name', 'color' => 'Color', 'size' => 'Size']) + ->setSyncRowKeysWithHeadingKeys(true) + ->addRow(['color' => 'Blue', 'name' => 'Fred', 'size' => 'Small']) + ->addRow(['size' => 'Large', 'age' => '24', 'name' => 'Mary']) + ->addRow(['color' => 'Green']); + +echo $table->generate(); +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameColorSize
FredBlueSmall
MaryLarge
Green
\ No newline at end of file From bde7c42e416686d3f647a9adf9231367dcc3b8d1 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Tue, 4 Apr 2023 20:39:54 +0200 Subject: [PATCH 087/485] More CS fixes (trailing comma in array) --- tests/system/View/TableTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/system/View/TableTest.php b/tests/system/View/TableTest.php index da567b63dbd6..5c574f2989a1 100644 --- a/tests/system/View/TableTest.php +++ b/tests/system/View/TableTest.php @@ -797,23 +797,23 @@ public function orderedColumnUsecases(): array 'reorder example #1' => [ 'heading' => ['id' => 'ID', 'name' => 'Name', 'age' => 'Age'], 'row' => ['name' => 'Max', 'age' => 30, 'id' => 5], - 'expectContainsString' => '5Max30' + 'expectContainsString' => '5Max30', ], 'reorder example #2' => [ 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'], 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5], - 'expectContainsString' => '530Fred' + 'expectContainsString' => '530Fred', ], '2 col heading, 3 col data row' => [ 'heading' => ['id' => 'ID', 'name' => 'Name'], 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5], - 'expectContainsString' => '5Fred' + 'expectContainsString' => '5Fred', ], '3 col heading, 2 col data row' => [ 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'], 'row' => ['name' => 'Fred', 'id' => 5], - 'expectContainsString' => '5Fred' - ] + 'expectContainsString' => '5Fred', + ], ]; } } From 90dfeee6ed6dee8b4361f284133960fc3a81d016 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Tue, 4 Apr 2023 22:59:20 +0200 Subject: [PATCH 088/485] Docu update with more example --- user_guide_src/source/outgoing/table.rst | 9 +++++++ user_guide_src/source/outgoing/table/020.php | 25 ++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 user_guide_src/source/outgoing/table/020.php diff --git a/user_guide_src/source/outgoing/table.rst b/user_guide_src/source/outgoing/table.rst index 8b876ac37e85..ce61a026697c 100644 --- a/user_guide_src/source/outgoing/table.rst +++ b/user_guide_src/source/outgoing/table.rst @@ -88,6 +88,15 @@ mentioned in heading, it places an empty cell for that spot. .. literalinclude:: table/019.php +.. important:: You must call ``setSyncRowKeysWithHeadingKeys(true)`` and + ``setHeading([...])`` before adding any rows via ``addRow([...])`` where + the rearrangement of columns takes place. + +You get the same result by using the result array is input in ``generate()`` + +.. literalinclude:: table/020.php + + *************** Class Reference *************** diff --git a/user_guide_src/source/outgoing/table/020.php b/user_guide_src/source/outgoing/table/020.php new file mode 100644 index 000000000000..5cfe457f523b --- /dev/null +++ b/user_guide_src/source/outgoing/table/020.php @@ -0,0 +1,25 @@ + 'Blue', + 'name' => 'Fred', + 'size' => 'Small', + ], + [ + 'size' => 'Large', + 'age' => '24', + 'name' => 'Mary', + ], + [ + 'color' => 'Green', + ], +]; + +$table = new \CodeIgniter\View\Table(); + +$table->setHeading(['name' => 'Name', 'color' => 'Color', 'size' => 'Size']) + ->setSyncRowKeysWithHeadingKeys(true); + +echo $table->generate($data); +?> \ No newline at end of file From 012bbf6b319a25e043490a9504c13781c3054797 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Tue, 4 Apr 2023 23:03:10 +0200 Subject: [PATCH 089/485] Fixing CS in docu --- user_guide_src/source/outgoing/table/020.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/outgoing/table/020.php b/user_guide_src/source/outgoing/table/020.php index 5cfe457f523b..fe7c123a475a 100644 --- a/user_guide_src/source/outgoing/table/020.php +++ b/user_guide_src/source/outgoing/table/020.php @@ -7,9 +7,9 @@ 'size' => 'Small', ], [ - 'size' => 'Large', - 'age' => '24', - 'name' => 'Mary', + 'size' => 'Large', + 'age' => '24', + 'name' => 'Mary', ], [ 'color' => 'Green', From d6099ff5637d5b13e1bc49decaba3146da1f084b Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Tue, 4 Apr 2023 23:06:40 +0200 Subject: [PATCH 090/485] Fixing CS in docu --- user_guide_src/source/outgoing/table/020.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/user_guide_src/source/outgoing/table/020.php b/user_guide_src/source/outgoing/table/020.php index fe7c123a475a..c34d97ffd3eb 100644 --- a/user_guide_src/source/outgoing/table/020.php +++ b/user_guide_src/source/outgoing/table/020.php @@ -21,5 +21,4 @@ $table->setHeading(['name' => 'Name', 'color' => 'Color', 'size' => 'Size']) ->setSyncRowKeysWithHeadingKeys(true); -echo $table->generate($data); -?> \ No newline at end of file +echo $table->generate($data); \ No newline at end of file From 0e4f7936030fddaaf7e85edec0c4bd93c943ae3b Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Tue, 4 Apr 2023 23:09:46 +0200 Subject: [PATCH 091/485] Fixing CS in docu --- user_guide_src/source/outgoing/table/020.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/outgoing/table/020.php b/user_guide_src/source/outgoing/table/020.php index c34d97ffd3eb..afada80c8d3c 100644 --- a/user_guide_src/source/outgoing/table/020.php +++ b/user_guide_src/source/outgoing/table/020.php @@ -21,4 +21,4 @@ $table->setHeading(['name' => 'Name', 'color' => 'Color', 'size' => 'Size']) ->setSyncRowKeysWithHeadingKeys(true); -echo $table->generate($data); \ No newline at end of file +echo $table->generate($data); From 3bfd3b8eafffa3a98927347fc95d5e22ff3a1ae0 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Wed, 5 Apr 2023 22:31:57 +0200 Subject: [PATCH 092/485] Renamed variable and method name --- system/View/Table.php | 10 +++++----- tests/system/View/TableTest.php | 4 ++-- user_guide_src/source/outgoing/table.rst | 10 +++++----- user_guide_src/source/outgoing/table/019.php | 4 ++-- user_guide_src/source/outgoing/table/020.php | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/system/View/Table.php b/system/View/Table.php index 94373f360d0e..14b147b0a0b5 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -86,7 +86,7 @@ class Table /** * Order each inserted row by heading keys */ - public bool $rowKeysSyncWithHeadingKeys = false; + public bool $syncRowsWithHeading = false; /** * Set the template from the table config file if it exists @@ -167,7 +167,7 @@ public function makeColumns($array = [], $columnLimit = 0) // Turn off the auto-heading feature since it's doubtful we // will want headings from a one-dimensional array $this->autoHeading = false; - $this->rowKeysSyncWithHeadingKeys = false; + $this->syncRowsWithHeading = false; if ($columnLimit === 0) { return $array; @@ -215,7 +215,7 @@ public function addRow() { $tmpRow = $this->_prepArgs(func_get_args()); - if ($this->rowKeysSyncWithHeadingKeys && ! empty($this->heading)) { + if ($this->syncRowsWithHeading && ! empty($this->heading)) { // each key has an index $keyIndex = array_flip(array_keys($this->heading)); @@ -242,9 +242,9 @@ public function addRow() * If a row has a key which does not exist in heading, it will be filtered out * If a row does not have a key which exists in heading, the field will stay empty */ - public function setSyncRowKeysWithHeadingKeys(bool $orderByKey): Table + public function setSyncRowsWithHeading(bool $orderByKey): Table { - $this->rowKeysSyncWithHeadingKeys = $orderByKey; + $this->syncRowsWithHeading = $orderByKey; return $this; } diff --git a/tests/system/View/TableTest.php b/tests/system/View/TableTest.php index 5c574f2989a1..b752b6c00ab4 100644 --- a/tests/system/View/TableTest.php +++ b/tests/system/View/TableTest.php @@ -770,7 +770,7 @@ public function testInvalidCallback() public function testAddRowAndGenerateWithOrderedColumns(array $heading, array $row, string $expectContainsString): void { $this->table->setHeading($heading); - $this->table->setSyncRowKeysWithHeadingKeys(true); + $this->table->setSyncRowsWithHeading(true); $this->table->addRow($row); $generated = $this->table->generate(); @@ -784,7 +784,7 @@ public function testAddRowAndGenerateWithOrderedColumns(array $heading, array $r public function testGenerateDataWithOrderedColumns(array $heading, array $row, string $expectContainsString): void { $this->table->setHeading($heading); - $this->table->setSyncRowKeysWithHeadingKeys(true); + $this->table->setSyncRowsWithHeading(true); $generated = $this->table->generate([$row]); diff --git a/user_guide_src/source/outgoing/table.rst b/user_guide_src/source/outgoing/table.rst index ce61a026697c..08477038ebd3 100644 --- a/user_guide_src/source/outgoing/table.rst +++ b/user_guide_src/source/outgoing/table.rst @@ -76,7 +76,7 @@ Synchronizing row columns with heading .. versionadded:: 4.4.0 -``setSyncRowKeysWithHeadingKeys(true)`` enables that each data value +``setSyncRowsWithHeading(true)`` enables that each data value is placed in the same column as defined in ``setHeading()`` if an associative array was used as parameter. This is especially useful when dealing with data loaded via REST API where the order is not to @@ -88,7 +88,7 @@ mentioned in heading, it places an empty cell for that spot. .. literalinclude:: table/019.php -.. important:: You must call ``setSyncRowKeysWithHeadingKeys(true)`` and +.. important:: You must call ``setSyncRowsWithHeading(true)`` and ``setHeading([...])`` before adding any rows via ``addRow([...])`` where the rearrangement of columns takes place. @@ -215,11 +215,11 @@ Class Reference .. literalinclude:: table/018.php - .. php:method:: setSyncRowKeysWithHeadingKeys(bool $orderByKey) + .. php:method:: setSyncRowsWithHeading(bool $orderByKey) :returns: Table instance (method chaining) :rtype: Table - Enables each row column to be ordered by heading keys. This gives - more control of how data is displayed in the final table. Make + Enables each row data key to be ordered by heading keys. This gives + more control of data being displaced in the correct column. Make sure to set this value before calling the first ``addRow()`` method. diff --git a/user_guide_src/source/outgoing/table/019.php b/user_guide_src/source/outgoing/table/019.php index 2edcac9c0197..5478867fddfa 100644 --- a/user_guide_src/source/outgoing/table/019.php +++ b/user_guide_src/source/outgoing/table/019.php @@ -3,7 +3,7 @@ $table = new \CodeIgniter\View\Table(); $table->setHeading(['name' => 'Name', 'color' => 'Color', 'size' => 'Size']) - ->setSyncRowKeysWithHeadingKeys(true) + ->setSyncRowsWithHeading(true) ->addRow(['color' => 'Blue', 'name' => 'Fred', 'size' => 'Small']) ->addRow(['size' => 'Large', 'age' => '24', 'name' => 'Mary']) ->addRow(['color' => 'Green']); @@ -37,4 +37,4 @@ - \ No newline at end of file + diff --git a/user_guide_src/source/outgoing/table/020.php b/user_guide_src/source/outgoing/table/020.php index afada80c8d3c..42bda4d2f3bc 100644 --- a/user_guide_src/source/outgoing/table/020.php +++ b/user_guide_src/source/outgoing/table/020.php @@ -19,6 +19,6 @@ $table = new \CodeIgniter\View\Table(); $table->setHeading(['name' => 'Name', 'color' => 'Color', 'size' => 'Size']) - ->setSyncRowKeysWithHeadingKeys(true); + ->setSyncRowsWithHeading(true); echo $table->generate($data); From 4a9bb60bb575f28a54701338814b86f3d4eb346b Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Wed, 5 Apr 2023 22:57:22 +0200 Subject: [PATCH 093/485] CS fixes after change --- system/View/Table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/View/Table.php b/system/View/Table.php index 14b147b0a0b5..76e43e26d393 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -166,7 +166,7 @@ public function makeColumns($array = [], $columnLimit = 0) // Turn off the auto-heading feature since it's doubtful we // will want headings from a one-dimensional array - $this->autoHeading = false; + $this->autoHeading = false; $this->syncRowsWithHeading = false; if ($columnLimit === 0) { From ddb6d3ff9e7249bded661c856db70fd942c6c631 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Thu, 6 Apr 2023 20:06:56 +0200 Subject: [PATCH 094/485] Added to changelog and suggested changes --- system/View/Table.php | 4 +++- tests/system/View/TableTest.php | 8 ++++---- user_guide_src/source/changelogs/v4.4.0.rst | 1 + user_guide_src/source/outgoing/table.rst | 16 +++++++++------- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/system/View/Table.php b/system/View/Table.php index 76e43e26d393..a7623ca616c8 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -241,8 +241,10 @@ public function addRow() * * If a row has a key which does not exist in heading, it will be filtered out * If a row does not have a key which exists in heading, the field will stay empty + * + * @return Table */ - public function setSyncRowsWithHeading(bool $orderByKey): Table + public function setSyncRowsWithHeading(bool $orderByKey) { $this->syncRowsWithHeading = $orderByKey; diff --git a/tests/system/View/TableTest.php b/tests/system/View/TableTest.php index b752b6c00ab4..2895f31df037 100644 --- a/tests/system/View/TableTest.php +++ b/tests/system/View/TableTest.php @@ -767,7 +767,7 @@ public function testInvalidCallback() /** * @dataProvider orderedColumnUsecases */ - public function testAddRowAndGenerateWithOrderedColumns(array $heading, array $row, string $expectContainsString): void + public function testAddRowAndGenerateOrderedColumns(array $heading, array $row, string $expectContainsString): void { $this->table->setHeading($heading); $this->table->setSyncRowsWithHeading(true); @@ -781,7 +781,7 @@ public function testAddRowAndGenerateWithOrderedColumns(array $heading, array $r /** * @dataProvider orderedColumnUsecases */ - public function testGenerateDataWithOrderedColumns(array $heading, array $row, string $expectContainsString): void + public function testGenerateOrderedColumns(array $heading, array $row, string $expectContainsString): void { $this->table->setHeading($heading); $this->table->setSyncRowsWithHeading(true); @@ -791,9 +791,9 @@ public function testGenerateDataWithOrderedColumns(array $heading, array $row, s $this->assertStringContainsString($expectContainsString, $generated); } - public function orderedColumnUsecases(): array + public function orderedColumnUsecases(): iterable { - return [ + yield from [ 'reorder example #1' => [ 'heading' => ['id' => 'ID', 'name' => 'Name', 'age' => 'Age'], 'row' => ['name' => 'Max', 'age' => 30, 'id' => 5], diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 503922b418e2..6e38883448ad 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -87,6 +87,7 @@ Others See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. +- **Table:** Addedd ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with heading. See :ref:`table-sync-rows-with-headings` for details. Message Changes *************** diff --git a/user_guide_src/source/outgoing/table.rst b/user_guide_src/source/outgoing/table.rst index 08477038ebd3..bec15f86dbbf 100644 --- a/user_guide_src/source/outgoing/table.rst +++ b/user_guide_src/source/outgoing/table.rst @@ -71,20 +71,22 @@ to the Table constructor: .. literalinclude:: table/008.php -Synchronizing row columns with heading -====================================== +.. _table-sync-rows-with-headings: + +Synchronizing Rows with Headings +================================ .. versionadded:: 4.4.0 -``setSyncRowsWithHeading(true)`` enables that each data value +The ``setSyncRowsWithHeading(true)`` method enables that each data value is placed in the same column as defined in ``setHeading()`` if an associative array was used as parameter. This is especially useful when dealing with data loaded via REST API where the order is not to your liking, or if the API returned too much data. -If a data row contains a ``key`` which does not exist the heading, -the value will be filtered out. Vise versa if a data row does not have key -mentioned in heading, it places an empty cell for that spot. +If a data row contains a key that is not present in the heading, its value is +filtered. Conversely, if a data row does not have a key listed in the heading, +an empty cell will be placed in its place. .. literalinclude:: table/019.php @@ -92,7 +94,7 @@ mentioned in heading, it places an empty cell for that spot. ``setHeading([...])`` before adding any rows via ``addRow([...])`` where the rearrangement of columns takes place. -You get the same result by using the result array is input in ``generate()`` +Using an array as input to ``generate()`` produces the same result: .. literalinclude:: table/020.php From 754391225db375b496a929393f113f2b2714afaf Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Thu, 6 Apr 2023 20:26:39 +0200 Subject: [PATCH 095/485] CS fix --- system/View/Table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/View/Table.php b/system/View/Table.php index a7623ca616c8..223d16db71e3 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -241,7 +241,7 @@ public function addRow() * * If a row has a key which does not exist in heading, it will be filtered out * If a row does not have a key which exists in heading, the field will stay empty - * + * * @return Table */ public function setSyncRowsWithHeading(bool $orderByKey) From e49decb75802fe0552019c6c8fc2f05242a5cb7a Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 3 Mar 2023 23:58:24 -0600 Subject: [PATCH 096/485] wip --- app/Config/Routes.php | 49 --------- app/Config/Routing.php | 100 ++++++++++++++++++ app/Routes.php | 7 ++ system/Config/Routing.php | 98 +++++++++++++++++ system/Config/Services.php | 2 +- system/Router/RouteCollection.php | 45 ++++++-- .../system/Router/AutoRouterImprovedTest.php | 2 +- .../RouteCollectionReverseRouteTest.php | 2 +- tests/system/Router/RouteCollectionTest.php | 2 +- user_guide_src/source/changelogs/v4.3.3.rst | 15 +++ user_guide_src/source/incoming/routing.rst | 25 ++--- .../source/incoming/routing/045.php | 3 +- .../source/incoming/routing/046.php | 1 + .../source/incoming/routing/049.php | 4 + .../source/incoming/routing/050.php | 4 + .../source/incoming/routing/051.php | 3 + .../source/installation/upgrade_433.rst | 9 ++ .../source/tutorial/static_pages.rst | 5 +- .../source/tutorial/static_pages/003.php | 2 - 19 files changed, 299 insertions(+), 79 deletions(-) delete mode 100644 app/Config/Routes.php create mode 100644 app/Config/Routing.php create mode 100644 app/Routes.php create mode 100644 system/Config/Routing.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php deleted file mode 100644 index c251ec22c4b4..000000000000 --- a/app/Config/Routes.php +++ /dev/null @@ -1,49 +0,0 @@ -setDefaultNamespace('App\Controllers'); -$routes->setDefaultController('Home'); -$routes->setDefaultMethod('index'); -$routes->setTranslateURIDashes(false); -$routes->set404Override(); -// The Auto Routing (Legacy) is very dangerous. It is easy to create vulnerable apps -// where controller filters or CSRF protection are bypassed. -// If you don't want to define all routes, please use the Auto Routing (Improved). -// Set `$autoRoutesImproved` to true in `app/Config/Feature.php` and set the following to true. -// $routes->setAutoRoute(false); - -/* - * -------------------------------------------------------------------- - * Route Definitions - * -------------------------------------------------------------------- - */ - -// We get a performance increase by specifying the default -// route since we don't have to scan directories. -$routes->get('/', 'Home::index'); - -/* - * -------------------------------------------------------------------- - * Additional Routing - * -------------------------------------------------------------------- - * - * There will often be times that you need additional routing and you - * need it to be able to override any defaults in this file. Environment - * based routes is one such time. require() additional route files here - * to make that happen. - * - * You will have access to the $routes object within that file without - * needing to reload it. - */ -if (is_file(APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php')) { - require APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php'; -} diff --git a/app/Config/Routing.php b/app/Config/Routing.php new file mode 100644 index 000000000000..1fc1b4b81d9b --- /dev/null +++ b/app/Config/Routing.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Config; + +use CodeIgniter\Config\Routing as BaseRouting; + +/** + * Routing configuration + */ +class Routing extends BaseRouting +{ + /** + * The default namespace to use for Controllers when no other + * namespace has been specified. + * + * Default: 'App\Controllers' + */ + public string $defaultNamespace = 'App\Controllers'; + + /** + * The default controller to use when no other controller has been + * specified. + * + * Default: 'Home' + */ + public string $defaultController = 'Home'; + + /** + * The default method to call on the controller when no other + * method has been set in the route. + * + * Default: 'index' + */ + public string $defaultMethod = 'index'; + + /** + * Whether to translate dashes in URIs to underscores. + * Primarily useful when using the auto-routing. + * + * Default: false + */ + public bool $translateURIDashes = false; + + /** + * Sets the class/method that should be called if routing doesn't + * find a match. It can be either a closure or the controller/method + * name exactly like a route is defined: Users::index + * + * This setting is passed to the Router class and handled there. + * + * If you want to use a closure, you will have to set it in the + * class constructor or the routes file by calling: + * + * $routes->set404Override(function() { + * // Do something here + * }); + * + * Example: + * public $override404 = 'App\Errors::show404'; + */ + public $override404 = null; + + /** + * If TRUE, the system will attempt to match the URI against + * Controllers by matching each segment against folders/files + * in APPPATH/Controllers, when a match wasn't found against + * defined routes. + * + * If FALSE, will stop searching and do NO automatic routing. + */ + public bool $autoRoute = false; + + /** + * If TRUE, will enable the use of the 'prioritize' option + * when defining routes. + * + * Default: false + */ + public bool $prioritize = false; + + /** + * An array of files that contain route definitions. + * Route files are read in order, with the first match + * found taking precedence. + * + * Default: APPPATH . 'Config/Routes.php' + */ + public array $routeFiles = [ + APPPATH .DIRECTORY_SEPARATOR . 'Routes.php', + ]; +} diff --git a/app/Routes.php b/app/Routes.php new file mode 100644 index 000000000000..6e7170b8a4c1 --- /dev/null +++ b/app/Routes.php @@ -0,0 +1,7 @@ +get('/', 'Home::index'); diff --git a/system/Config/Routing.php b/system/Config/Routing.php new file mode 100644 index 000000000000..bf5249377a08 --- /dev/null +++ b/system/Config/Routing.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Config; + +/** + * Routing configuration + */ +class Routing extends BaseConfig +{ + /** + * The default namespace to use for Controllers when no other + * namespace has been specified. + * + * Default: 'App\Controllers' + */ + public string $defaultNamespace = 'App\Controllers'; + + /** + * The default controller to use when no other controller has been + * specified. + * + * Default: 'Home' + */ + public string $defaultController = 'Home'; + + /** + * The default method to call on the controller when no other + * method has been set in the route. + * + * Default: 'index' + */ + public string $defaultMethod = 'index'; + + /** + * Whether to translate dashes in URIs to underscores. + * Primarily useful when using the auto-routing. + * + * Default: false + */ + public bool $translateURIDashes = false; + + /** + * Sets the class/method that should be called if routing doesn't + * find a match. It can be either a closure or the controller/method + * name exactly like a route is defined: Users::index + * + * This setting is passed to the Router class and handled there. + * + * If you want to use a closure, you will have to set it in the + * class constructor or the routes file by calling: + * + * $routes->set404Override(function() { + * // Do something here + * }); + * + * Example: + * public $override404 = 'App\Errors::show404'; + */ + public $override404 = null; + + /** + * If TRUE, the system will attempt to match the URI against + * Controllers by matching each segment against folders/files + * in APPPATH/Controllers, when a match wasn't found against + * defined routes. + * + * If FALSE, will stop searching and do NO automatic routing. + */ + public bool $autoRoute = false; + + /** + * If TRUE, will enable the use of the 'prioritize' option + * when defining routes. + * + * Default: false + */ + public bool $prioritize = false; + + /** + * An array of files that contain route definitions. + * Route files are read in order, with the first match + * found taking precedence. + * + * Default: APPPATH . 'Config/Routes.php' + */ + public array $routeFiles = [ + APPPATH .DIRECTORY_SEPARATOR . 'Routes.php', + ]; +} diff --git a/system/Config/Services.php b/system/Config/Services.php index 2358e87bb1a5..161a6b112757 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -596,7 +596,7 @@ public static function routes(bool $getShared = true) return static::getSharedInstance('routes'); } - return new RouteCollection(AppServices::locator(), config('Modules')); + return new RouteCollection(AppServices::locator(), config('Modules'), config('Routing')); } /** diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 50b73616ea0e..49410803e776 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -16,6 +16,7 @@ use CodeIgniter\Router\Exceptions\RouterException; use Config\App; use Config\Modules; +use Config\Routing; use Config\Services; use InvalidArgumentException; use Locale; @@ -88,6 +89,12 @@ class RouteCollection implements RouteCollectionInterface */ protected $override404; + /** + * An array of files that would contain route definitions. + * @var array + */ + protected array $routeFiles = []; + /** * Defined placeholders that can be used * within the @@ -242,12 +249,22 @@ class RouteCollection implements RouteCollectionInterface /** * Constructor */ - public function __construct(FileLocator $locator, Modules $moduleConfig) + public function __construct(FileLocator $locator, Modules $moduleConfig, Routing $routing) { $this->fileLocator = $locator; $this->moduleConfig = $moduleConfig; $this->httpHost = Services::request()->getServer('HTTP_HOST'); + + // Setup based on config file. Let routes file override. + $this->defaultNamespace = $routing->defaultNamespace; + $this->defaultController = $routing->defaultController; + $this->defaultMethod = $routing->defaultMethod; + $this->translateURIDashes = $routing->translateURIDashes; + $this->override404 = $routing->override404; + $this->autoRoute = $routing->autoRoute; + $this->routeFiles = $routing->routeFiles; + $this->prioritize = $routing->prioritize; } /** @@ -263,8 +280,25 @@ public function loadRoutes(string $routesFile = APPPATH . 'Config/Routes.php') return $this; } + // Include the passed in routesFile if it doesn't exist. + // Only keeping that around for BC purposes for now. + $routeFiles = $this->routeFiles; + if (! in_array($routesFile, $routeFiles, true)) { + $routeFiles[] = $routesFile; + } + + // We need this var in local scope + // so route files can access it. $routes = $this; - require $routesFile; + + foreach($routeFiles as $routesFile) { + if (! is_file($routesFile)) { + log_message('warning', 'Routes file not found: ' . $routesFile . '.'); + continue; + } + + require $routesFile; + } $this->discoverRoutes(); @@ -288,14 +322,9 @@ protected function discoverRoutes() if ($this->moduleConfig->shouldDiscover('routes')) { $files = $this->fileLocator->search('Config/Routes.php'); - $excludes = [ - APPPATH . 'Config' . DIRECTORY_SEPARATOR . 'Routes.php', - SYSTEMPATH . 'Config' . DIRECTORY_SEPARATOR . 'Routes.php', - ]; - foreach ($files as $file) { // Don't include our main file again... - if (in_array($file, $excludes, true)) { + if (in_array($file, $this->routeFiles, true)) { continue; } diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index ab1fa7db2188..a460cfdfd56c 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -35,7 +35,7 @@ protected function setUp(): void $moduleConfig = new Modules(); $moduleConfig->enabled = false; - $this->collection = new RouteCollection(Services::locator(), $moduleConfig); + $this->collection = new RouteCollection(Services::locator(), $moduleConfig, new \Config\Routing()); } private function createNewAutoRouter(string $httpVerb = 'get'): AutoRouterImproved diff --git a/tests/system/Router/RouteCollectionReverseRouteTest.php b/tests/system/Router/RouteCollectionReverseRouteTest.php index 2e6de5aacdf7..a5ac40e47cfa 100644 --- a/tests/system/Router/RouteCollectionReverseRouteTest.php +++ b/tests/system/Router/RouteCollectionReverseRouteTest.php @@ -49,7 +49,7 @@ protected function getCollector(array $config = [], array $files = [], $moduleCo $moduleConfig->enabled = false; } - return (new RouteCollection($loader, $moduleConfig))->setHTTPVerb('get'); + return (new RouteCollection($loader, $moduleConfig, new \Config\Routing()))->setHTTPVerb('get'); } public function testReverseRoutingFindsSimpleMatch() diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 6458449a02c9..abb2feb2e4f4 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -49,7 +49,7 @@ protected function getCollector(array $config = [], array $files = [], $moduleCo $moduleConfig->enabled = false; } - return (new RouteCollection($loader, $moduleConfig))->setHTTPVerb('get'); + return (new RouteCollection($loader, $moduleConfig, new \Config\Routing()))->setHTTPVerb('get'); } public function testBasicAdd() diff --git a/user_guide_src/source/changelogs/v4.3.3.rst b/user_guide_src/source/changelogs/v4.3.3.rst index cd9d3fc31a40..3d0c48249ffc 100644 --- a/user_guide_src/source/changelogs/v4.3.3.rst +++ b/user_guide_src/source/changelogs/v4.3.3.rst @@ -16,6 +16,21 @@ SECURITY - **Text Helper:** The :php:func:`random_string()` type **alpha**, **alnum**, **numeric** and **nozero** are now cryptographically secure. +BREAKING +******** + +Message Changes +*************** + +Changes +******* + +- **Config:** Routing settings have been moved to ``Config\Routing`` config file. +- The default location for new projects Routes.php file has been moved to `app/Routes.php`. This can be modified in the `Config/Routing.php` file. + +Deprecations +************ + Bugs Fixed ********** diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index e11dee96c818..cee4c6ba672a 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -22,7 +22,7 @@ First, let's look at Defined Route Routing. If you want to use Auto Routing, see Setting Routing Rules ********************* -Routing rules are defined in the **app/Config/Routes.php** file. In it you'll see that +Routing rules are defined in the **app/Routes.php** file. In it you'll see that it creates an instance of the RouteCollection class (``$routes``) that permits you to specify your own routing criteria. Routes can be specified using placeholders or Regular Expressions. @@ -408,7 +408,7 @@ The value for the filter can be a string or an array of strings: See :doc:`Controller Filters ` for more information on setting up filters. -.. Warning:: If you set filters to routes in **app/Config/Routes.php** +.. Warning:: If you set filters to routes in **app/Routes.php** (not in **app/Config/Filters.php**), it is recommended to disable Auto Routing (Legacy). When :ref:`auto-routing-legacy` is enabled, it may be possible that a controller can be accessed via a different URL than the configured route, @@ -545,7 +545,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 in **app/Config/Routing.php**. .. _routing-default-namespace: @@ -569,8 +569,7 @@ Translate URI Dashes ==================== This option enables you to automatically replace dashes (``-``) with underscores in the controller and method -URI segments, thus saving you additional route entries if you need to do that. This is required because the -dash isn't a valid class or method name character and would cause a fatal error if you try to use it: +URI segments when used in Auto Routing, thus saving you additional route entries if you need to do that. This is required because the dash isn't a valid class or method name character and would cause a fatal error if you try to use it: .. literalinclude:: routing/049.php @@ -585,7 +584,7 @@ When no defined route is found that matches the URI, the system will attempt to controllers and methods when Auto Routing is enabled. You can disable this automatic matching, and restrict routes -to only those defined by you, by setting the ``setAutoRoute()`` option to false: +to only those defined by you, by setting the ``$autoRoute`` option to false: .. literalinclude:: routing/050.php @@ -601,6 +600,8 @@ a valid class/method pair, just like you would show in any route, or a Closure: .. literalinclude:: routing/051.php +Using the ``set404Override`` method within the routes file, you can use closures. Defining the override in the Routing file is restricted to class/method pairs. + .. note:: The ``set404Override()`` method does not change the Response status code to ``404``. If you don't set the status code in the controller you set, the default status code ``200`` will be returned. See :php:meth:`CodeIgniter\\HTTP\\Response::setStatusCode()` for @@ -642,9 +643,9 @@ and execute the corresponding controller methods. Enable Auto Routing =================== -To use it, you need to change the setting ``setAutoRoute()`` option to true in **app/Config/Routes.php**:: +To use it, you need to change the setting ``$autoRoute`` option to true in **app/Config/Routing.php**:: - $routes->setAutoRoute(true); + public bool $autoRoute = true; And you need to change the property ``$autoRoutesImproved`` to ``true`` in **app/Config/Feature.php**:: @@ -676,7 +677,7 @@ See :ref:`Auto Routing in Controllers ` for mo Configuration Options ===================== -These options are available at the top of **app/Config/Routes.php**. +These options are available at the top of **app/Routes.php**. Default Controller ------------------ @@ -719,7 +720,7 @@ Auto Routing (Legacy) Auto Routing (Legacy) is a routing system from CodeIgniter 3. It can automatically route HTTP requests based on conventions and execute the corresponding controller methods. -It is recommended that all routes are defined in the **app/Config/Routes.php** file, +It is recommended that all routes are defined in the **app/Routes.php** file, or to use :ref:`auto-routing-improved`, .. warning:: To prevent misconfiguration and miscoding, we recommend that you do not use @@ -733,7 +734,7 @@ Enable Auto Routing (Legacy) Since v4.2.0, the auto-routing is disabled by default. -To use it, you need to change the setting ``setAutoRoute()`` option to true in **app/Config/Routes.php**:: +To use it, you need to change the setting ``setAutoRoute()`` option to true in **app/Routes.php**:: $routes->setAutoRoute(true); @@ -760,7 +761,7 @@ See :ref:`Auto Routing (Legacy) in Controllers ` Configuration Options (Legacy) ============================== -These options are available at the top of **app/Config/Routes.php**. +These options are available at the top of **app/Routes.php**. Default Controller (Legacy) --------------------------- diff --git a/user_guide_src/source/incoming/routing/045.php b/user_guide_src/source/incoming/routing/045.php index 98aef9276f9d..f20d34a40681 100644 --- a/user_guide_src/source/incoming/routing/045.php +++ b/user_guide_src/source/incoming/routing/045.php @@ -1,6 +1,7 @@ setDefaultNamespace(''); +// In app/Config/Routing.php +public string $defaultNamespace = ''; // Controller is \Users $routes->get('users', 'Users::index'); diff --git a/user_guide_src/source/incoming/routing/046.php b/user_guide_src/source/incoming/routing/046.php index fbbbee2300df..8998fe70c039 100644 --- a/user_guide_src/source/incoming/routing/046.php +++ b/user_guide_src/source/incoming/routing/046.php @@ -1,5 +1,6 @@ setDefaultNamespace('App'); // Controller is \App\Users diff --git a/user_guide_src/source/incoming/routing/049.php b/user_guide_src/source/incoming/routing/049.php index 31d49508ab86..57bee07fcb67 100644 --- a/user_guide_src/source/incoming/routing/049.php +++ b/user_guide_src/source/incoming/routing/049.php @@ -1,3 +1,7 @@ setTranslateURIDashes(true); diff --git a/user_guide_src/source/incoming/routing/050.php b/user_guide_src/source/incoming/routing/050.php index c331102cd9f4..8133cf77b7f1 100644 --- a/user_guide_src/source/incoming/routing/050.php +++ b/user_guide_src/source/incoming/routing/050.php @@ -1,3 +1,7 @@ setAutoRoute(false); diff --git a/user_guide_src/source/incoming/routing/051.php b/user_guide_src/source/incoming/routing/051.php index dddd067f0f26..098d85178575 100644 --- a/user_guide_src/source/incoming/routing/051.php +++ b/user_guide_src/source/incoming/routing/051.php @@ -1,5 +1,8 @@ set404Override('App\Errors::show404'); diff --git a/user_guide_src/source/installation/upgrade_433.rst b/user_guide_src/source/installation/upgrade_433.rst index 5e86a9737767..e7f830d7d5a7 100644 --- a/user_guide_src/source/installation/upgrade_433.rst +++ b/user_guide_src/source/installation/upgrade_433.rst @@ -27,6 +27,15 @@ 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: +Routing +------- + +To clean up the routing system, the following changes were made: + - New ``app/Config/Routing.php`` file that holds the settings that used to be in the Routes file. + - The ``app/Config/Routes.php`` file was simplified so that it only contains the routes without settings and verbiage to clutter the file. + - The ``app/Config/Routes.php`` file was moved to ``app/Routes.php`` to make it easier to find. When upgrading, you can change the ``app/Config/Routing.php` file, ``$routeFiles`` property to point to the old location if you prefer. + - The environment-specific routes files are no longer loaded automatically. To load those, you must add them to the ``$routeFiles`` property in ``app/Config/Routing.php``. + Config ------ diff --git a/user_guide_src/source/tutorial/static_pages.rst b/user_guide_src/source/tutorial/static_pages.rst index 926ed09603c7..37c6121129de 100644 --- a/user_guide_src/source/tutorial/static_pages.rst +++ b/user_guide_src/source/tutorial/static_pages.rst @@ -136,10 +136,9 @@ We have made the controller. The next thing is to set routing rules. Routing associates a URI with a controller's method. Let's do that. Open the routing file located at -**app/Config/Routes.php** and look for the "Route Definitions" -section of the configuration file. +**app/Routes.php**. -The only uncommented line there to start with should be: +The only line there to start with should be: .. literalinclude:: static_pages/003.php diff --git a/user_guide_src/source/tutorial/static_pages/003.php b/user_guide_src/source/tutorial/static_pages/003.php index 956c097d390f..bf0466ca2192 100644 --- a/user_guide_src/source/tutorial/static_pages/003.php +++ b/user_guide_src/source/tutorial/static_pages/003.php @@ -2,8 +2,6 @@ // ... -// We get a performance increase by specifying the default -// route since we don't have to scan directories. $routes->get('/', 'Home::index'); // ... From 481c2f983ac233bae492efe6d53c98e65e762e57 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sat, 25 Mar 2023 00:09:02 -0500 Subject: [PATCH 097/485] refactor: Clean up routes file to be just for routes. Moved route settings to new Routing config file. --- app/Config/Routing.php | 22 ++++++++++----------- system/Config/Routing.php | 22 ++++++++++----------- system/Router/RouteCollection.php | 2 +- tests/system/Router/RouteCollectionTest.php | 5 ++++- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/app/Config/Routing.php b/app/Config/Routing.php index 1fc1b4b81d9b..2cf88bf9b29b 100644 --- a/app/Config/Routing.php +++ b/app/Config/Routing.php @@ -18,6 +18,17 @@ */ class Routing extends BaseRouting { + /** + * An array of files that contain route definitions. + * Route files are read in order, with the first match + * found taking precedence. + * + * Default: APPPATH . 'Config/Routes.php' + */ + public array $routeFiles = [ + APPPATH . 'Routes.php', + ]; + /** * The default namespace to use for Controllers when no other * namespace has been specified. @@ -86,15 +97,4 @@ class Routing extends BaseRouting * Default: false */ public bool $prioritize = false; - - /** - * An array of files that contain route definitions. - * Route files are read in order, with the first match - * found taking precedence. - * - * Default: APPPATH . 'Config/Routes.php' - */ - public array $routeFiles = [ - APPPATH .DIRECTORY_SEPARATOR . 'Routes.php', - ]; } diff --git a/system/Config/Routing.php b/system/Config/Routing.php index bf5249377a08..580e555d202d 100644 --- a/system/Config/Routing.php +++ b/system/Config/Routing.php @@ -16,6 +16,17 @@ */ class Routing extends BaseConfig { + /** + * An array of files that contain route definitions. + * Route files are read in order, with the first match + * found taking precedence. + * + * Default: APPPATH . 'Config/Routes.php' + */ + public array $routeFiles = [ + APPPATH . 'Routes.php', + ]; + /** * The default namespace to use for Controllers when no other * namespace has been specified. @@ -84,15 +95,4 @@ class Routing extends BaseConfig * Default: false */ public bool $prioritize = false; - - /** - * An array of files that contain route definitions. - * Route files are read in order, with the first match - * found taking precedence. - * - * Default: APPPATH . 'Config/Routes.php' - */ - public array $routeFiles = [ - APPPATH .DIRECTORY_SEPARATOR . 'Routes.php', - ]; } diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 49410803e776..b5f06d936713 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -274,7 +274,7 @@ public function __construct(FileLocator $locator, Modules $moduleConfig, Routing * * @return $this */ - public function loadRoutes(string $routesFile = APPPATH . 'Config/Routes.php') + public function loadRoutes(string $routesFile = APPPATH . 'Routes.php') { if ($this->didDiscover) { return $this; diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index abb2feb2e4f4..3802d067200d 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -49,7 +49,10 @@ protected function getCollector(array $config = [], array $files = [], $moduleCo $moduleConfig->enabled = false; } - return (new RouteCollection($loader, $moduleConfig, new \Config\Routing()))->setHTTPVerb('get'); + $routerConfig = new \Config\Routing(); + $routerConfig->defaultNamespace = '\\'; + + return (new RouteCollection($loader, $moduleConfig, $routerConfig))->setHTTPVerb('get'); } public function testBasicAdd() From 31138abda5aaf45a05c8e25eca6ece237548f79f Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sat, 25 Mar 2023 00:24:38 -0500 Subject: [PATCH 098/485] Fixing style issues --- app/Config/Routing.php | 2 +- app/Routes.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Config/Routing.php b/app/Config/Routing.php index 2cf88bf9b29b..f6163000efdb 100644 --- a/app/Config/Routing.php +++ b/app/Config/Routing.php @@ -78,7 +78,7 @@ class Routing extends BaseRouting * Example: * public $override404 = 'App\Errors::show404'; */ - public $override404 = null; + public $override404; /** * If TRUE, the system will attempt to match the URI against diff --git a/app/Routes.php b/app/Routes.php index 6e7170b8a4c1..4fb22a2e358c 100644 --- a/app/Routes.php +++ b/app/Routes.php @@ -3,5 +3,4 @@ /** * @var \CodeIgniter\Router\RouteCollection $routes */ - $routes->get('/', 'Home::index'); From e37c2c616396f01cc2a8cb7a35438de58d81b653 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sat, 25 Mar 2023 00:31:05 -0500 Subject: [PATCH 099/485] More style fixes --- system/Config/Routing.php | 2 +- system/Router/RouteCollection.php | 4 ++-- tests/system/Router/RouteCollectionTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/system/Config/Routing.php b/system/Config/Routing.php index 580e555d202d..857a27769215 100644 --- a/system/Config/Routing.php +++ b/system/Config/Routing.php @@ -76,7 +76,7 @@ class Routing extends BaseConfig * Example: * public $override404 = 'App\Errors::show404'; */ - public $override404 = null; + public $override404; /** * If TRUE, the system will attempt to match the URI against diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index b5f06d936713..9962bb23e11c 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -91,7 +91,6 @@ class RouteCollection implements RouteCollectionInterface /** * An array of files that would contain route definitions. - * @var array */ protected array $routeFiles = []; @@ -291,9 +290,10 @@ public function loadRoutes(string $routesFile = APPPATH . 'Routes.php') // so route files can access it. $routes = $this; - foreach($routeFiles as $routesFile) { + foreach ($routeFiles as $routesFile) { if (! is_file($routesFile)) { log_message('warning', 'Routes file not found: ' . $routesFile . '.'); + continue; } diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 3802d067200d..e298ea723b08 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -49,7 +49,7 @@ protected function getCollector(array $config = [], array $files = [], $moduleCo $moduleConfig->enabled = false; } - $routerConfig = new \Config\Routing(); + $routerConfig = new \Config\Routing(); $routerConfig->defaultNamespace = '\\'; return (new RouteCollection($loader, $moduleConfig, $routerConfig))->setHTTPVerb('get'); From ec0da198627a277f8873bd13ffe970e43e72b470 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 31 Mar 2023 22:34:24 -0500 Subject: [PATCH 100/485] Allow specifying the location of Routing files within modules. --- system/Config/Routing.php | 10 ++++++++++ system/Router/RouteCollection.php | 14 +++++++++++++- tests/system/Router/RouteCollectionTest.php | 1 + tests/system/Router/RouterTest.php | 7 ++++++- user_guide_src/source/general/modules.rst | 2 ++ user_guide_src/source/installation/upgrade_433.rst | 1 + 6 files changed, 33 insertions(+), 2 deletions(-) diff --git a/system/Config/Routing.php b/system/Config/Routing.php index 857a27769215..158a7db3c66f 100644 --- a/system/Config/Routing.php +++ b/system/Config/Routing.php @@ -27,6 +27,16 @@ class Routing extends BaseConfig APPPATH . 'Routes.php', ]; + /** + * When discovering routes within "modules", or namespaces other + * than "App", this is the path relative to the module's root + * directory. + * + * Default: 'Routes.php' + * Legacy: 'Config/Routes.php' + */ + public string $modulePath = 'Routes.php'; + /** * The default namespace to use for Controllers when no other * namespace has been specified. diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 9962bb23e11c..c8650c217a84 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -94,6 +94,12 @@ class RouteCollection implements RouteCollectionInterface */ protected array $routeFiles = []; + /** + * The path to the file that contains + * the route definitions within "modules". + */ + protected string $modulePath; + /** * Defined placeholders that can be used * within the @@ -263,6 +269,7 @@ public function __construct(FileLocator $locator, Modules $moduleConfig, Routing $this->override404 = $routing->override404; $this->autoRoute = $routing->autoRoute; $this->routeFiles = $routing->routeFiles; + $this->modulePath = $routing->modulePath; $this->prioritize = $routing->prioritize; } @@ -320,7 +327,12 @@ protected function discoverRoutes() $routes = $this; if ($this->moduleConfig->shouldDiscover('routes')) { - $files = $this->fileLocator->search('Config/Routes.php'); + if (empty($this->modulePath)) { + log_message('warning', 'Routes module path is not set in Config/Routing.php.'); + return; + } + + $files = $this->fileLocator->search($this->modulePath); foreach ($files as $file) { // Don't include our main file again... diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index e298ea723b08..bfe5378b60d0 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -51,6 +51,7 @@ protected function getCollector(array $config = [], array $files = [], $moduleCo $routerConfig = new \Config\Routing(); $routerConfig->defaultNamespace = '\\'; + $routerConfig->modulePath = 'Config/Routes.php'; return (new RouteCollection($loader, $moduleConfig, $routerConfig))->setHTTPVerb('get'); } diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php index 5be01a47bfc6..9cf8c90bb9a7 100644 --- a/tests/system/Router/RouterTest.php +++ b/tests/system/Router/RouterTest.php @@ -36,7 +36,12 @@ protected function setUp(): void $moduleConfig = new Modules(); $moduleConfig->enabled = false; - $this->collection = new RouteCollection(Services::locator(), $moduleConfig); + + $routerConfig = new \Config\Routing(); + $routerConfig->defaultNamespace = '\\'; + $routerConfig->modulePath = 'Config/Routes.php'; + + $this->collection = new RouteCollection(Services::locator(), $moduleConfig, $routerConfig); $routes = [ '/' => 'Home::index', diff --git a/user_guide_src/source/general/modules.rst b/user_guide_src/source/general/modules.rst index 2298c32a986f..40831cb76121 100644 --- a/user_guide_src/source/general/modules.rst +++ b/user_guide_src/source/general/modules.rst @@ -154,6 +154,8 @@ the **Modules** config file, described above. When working with modules, it can be a problem if the routes in the application contain wildcards. In that case, see :ref:`routing-priority`. +By default, route files are named **Routes.php** and are located in the root directory of the module. You can change this by setting the ``$modulePath`` variable in the **Routing** config file to path to the file, relative to the module's root directory. For example, if you wanted to put your routes in a file named **Routes.php** in the module's ``Config`` directory, you would set the ``$modulePath`` variable to ``Config/Routes.php``. + Filters ======= diff --git a/user_guide_src/source/installation/upgrade_433.rst b/user_guide_src/source/installation/upgrade_433.rst index e7f830d7d5a7..4b32c01433d3 100644 --- a/user_guide_src/source/installation/upgrade_433.rst +++ b/user_guide_src/source/installation/upgrade_433.rst @@ -34,6 +34,7 @@ To clean up the routing system, the following changes were made: - New ``app/Config/Routing.php`` file that holds the settings that used to be in the Routes file. - The ``app/Config/Routes.php`` file was simplified so that it only contains the routes without settings and verbiage to clutter the file. - The ``app/Config/Routes.php`` file was moved to ``app/Routes.php`` to make it easier to find. When upgrading, you can change the ``app/Config/Routing.php` file, ``$routeFiles`` property to point to the old location if you prefer. + - Any module ``Routes.php`` files are expected to be in the namespace's root directory now. To adjust this to match the functionality of existing projects, you can cahnge the ``$modulePath`` property in ``app/Config/Routing.php`` to ``'Config/Routes.php'``. - The environment-specific routes files are no longer loaded automatically. To load those, you must add them to the ``$routeFiles`` property in ``app/Config/Routing.php``. Config From 520a0a23789672ee9b9dc4e92c49879c162f2325 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 31 Mar 2023 23:19:03 -0500 Subject: [PATCH 101/485] Change user guide changes to 4.4 --- user_guide_src/source/changelogs/v4.3.3.rst | 3 --- user_guide_src/source/changelogs/v4.4.0.rst | 2 ++ user_guide_src/source/installation/upgrade_433.rst | 10 ---------- user_guide_src/source/installation/upgrade_440.rst | 10 ++++++++++ 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.3.3.rst b/user_guide_src/source/changelogs/v4.3.3.rst index 3d0c48249ffc..1430cf767aea 100644 --- a/user_guide_src/source/changelogs/v4.3.3.rst +++ b/user_guide_src/source/changelogs/v4.3.3.rst @@ -25,9 +25,6 @@ Message Changes Changes ******* -- **Config:** Routing settings have been moved to ``Config\Routing`` config file. -- The default location for new projects Routes.php file has been moved to `app/Routes.php`. This can be modified in the `Config/Routing.php` file. - Deprecations ************ diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 503922b418e2..74c59205ddb4 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -104,6 +104,8 @@ Changes So if you installed CodeIgniter under the folder that contains the special characters like ``(``, ``)``, etc., CodeIgniter didn't work. Since v4.4.0, this restriction has been removed. +- **Config:** Routing settings have been moved to ``Config\Routing`` config file. +- The default location for new projects Routes.php file has been moved to `app/Routes.php`. This can be modified in the `Config/Routing.php` file. Deprecations ************ diff --git a/user_guide_src/source/installation/upgrade_433.rst b/user_guide_src/source/installation/upgrade_433.rst index 4b32c01433d3..5e86a9737767 100644 --- a/user_guide_src/source/installation/upgrade_433.rst +++ b/user_guide_src/source/installation/upgrade_433.rst @@ -27,16 +27,6 @@ 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: -Routing -------- - -To clean up the routing system, the following changes were made: - - New ``app/Config/Routing.php`` file that holds the settings that used to be in the Routes file. - - The ``app/Config/Routes.php`` file was simplified so that it only contains the routes without settings and verbiage to clutter the file. - - The ``app/Config/Routes.php`` file was moved to ``app/Routes.php`` to make it easier to find. When upgrading, you can change the ``app/Config/Routing.php` file, ``$routeFiles`` property to point to the old location if you prefer. - - Any module ``Routes.php`` files are expected to be in the namespace's root directory now. To adjust this to match the functionality of existing projects, you can cahnge the ``$modulePath`` property in ``app/Config/Routing.php`` to ``'Config/Routes.php'``. - - The environment-specific routes files are no longer loaded automatically. To load those, you must add them to the ``$routeFiles`` property in ``app/Config/Routing.php``. - Config ------ diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index f5bc7aef91f2..4e2ec61c5f7d 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -62,6 +62,16 @@ 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: +Routing +------- + +To clean up the routing system, the following changes were made: + - New ``app/Config/Routing.php`` file that holds the settings that used to be in the Routes file. + - The ``app/Config/Routes.php`` file was simplified so that it only contains the routes without settings and verbiage to clutter the file. + - The ``app/Config/Routes.php`` file was moved to ``app/Routes.php`` to make it easier to find. When upgrading, you can change the ``app/Config/Routing.php` file, ``$routeFiles`` property to point to the old location if you prefer. + - Any module ``Routes.php`` files are expected to be in the namespace's root directory now. To adjust this to match the functionality of existing projects, you can cahnge the ``$modulePath`` property in ``app/Config/Routing.php`` to ``'Config/Routes.php'``. + - The environment-specific routes files are no longer loaded automatically. To load those, you must add them to the ``$routeFiles`` property in ``app/Config/Routing.php``. + Config ------ From 0604b40afa1db535a7ed9c5cb272ffecd18fefaa Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 3 Apr 2023 23:10:55 -0500 Subject: [PATCH 102/485] Move routes files back to Config folder for now, and drop the modulesPath config setting. --- app/{ => Config}/Routes.php | 0 app/Config/Routing.php | 2 +- system/Config/Routing.php | 10 ---------- system/Router/RouteCollection.php | 14 +------------- .../Commands/Utilities/Routes/FilterFinderTest.php | 3 ++- tests/system/Router/RouteCollectionTest.php | 1 - tests/system/Router/RouterTest.php | 1 - user_guide_src/source/changelogs/v4.4.0.rst | 1 - user_guide_src/source/general/modules.rst | 2 -- user_guide_src/source/installation/upgrade_440.rst | 1 - 10 files changed, 4 insertions(+), 31 deletions(-) rename app/{ => Config}/Routes.php (100%) diff --git a/app/Routes.php b/app/Config/Routes.php similarity index 100% rename from app/Routes.php rename to app/Config/Routes.php diff --git a/app/Config/Routing.php b/app/Config/Routing.php index f6163000efdb..73d9a93f0f4b 100644 --- a/app/Config/Routing.php +++ b/app/Config/Routing.php @@ -26,7 +26,7 @@ class Routing extends BaseRouting * Default: APPPATH . 'Config/Routes.php' */ public array $routeFiles = [ - APPPATH . 'Routes.php', + APPPATH . 'Config/Routes.php', ]; /** diff --git a/system/Config/Routing.php b/system/Config/Routing.php index 158a7db3c66f..857a27769215 100644 --- a/system/Config/Routing.php +++ b/system/Config/Routing.php @@ -27,16 +27,6 @@ class Routing extends BaseConfig APPPATH . 'Routes.php', ]; - /** - * When discovering routes within "modules", or namespaces other - * than "App", this is the path relative to the module's root - * directory. - * - * Default: 'Routes.php' - * Legacy: 'Config/Routes.php' - */ - public string $modulePath = 'Routes.php'; - /** * The default namespace to use for Controllers when no other * namespace has been specified. diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index c8650c217a84..9962bb23e11c 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -94,12 +94,6 @@ class RouteCollection implements RouteCollectionInterface */ protected array $routeFiles = []; - /** - * The path to the file that contains - * the route definitions within "modules". - */ - protected string $modulePath; - /** * Defined placeholders that can be used * within the @@ -269,7 +263,6 @@ public function __construct(FileLocator $locator, Modules $moduleConfig, Routing $this->override404 = $routing->override404; $this->autoRoute = $routing->autoRoute; $this->routeFiles = $routing->routeFiles; - $this->modulePath = $routing->modulePath; $this->prioritize = $routing->prioritize; } @@ -327,12 +320,7 @@ protected function discoverRoutes() $routes = $this; if ($this->moduleConfig->shouldDiscover('routes')) { - if (empty($this->modulePath)) { - log_message('warning', 'Routes module path is not set in Config/Routing.php.'); - return; - } - - $files = $this->fileLocator->search($this->modulePath); + $files = $this->fileLocator->search('Config/Routes.php'); foreach ($files as $file) { // Don't include our main file again... diff --git a/tests/system/Commands/Utilities/Routes/FilterFinderTest.php b/tests/system/Commands/Utilities/Routes/FilterFinderTest.php index ace71d9d7e2d..91d73153dc2d 100644 --- a/tests/system/Commands/Utilities/Routes/FilterFinderTest.php +++ b/tests/system/Commands/Utilities/Routes/FilterFinderTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Commands\Utilities\Routes; +use Config\Routing; use CodeIgniter\Config\Services; use CodeIgniter\Filters\CSRF; use CodeIgniter\Filters\DebugToolbar; @@ -52,7 +53,7 @@ protected function setUp(): void private function createRouteCollection(array $routes = []): RouteCollection { - $collection = new RouteCollection(Services::locator(), $this->moduleConfig); + $collection = new RouteCollection(Services::locator(), $this->moduleConfig, new Routing()); $routes = ($routes !== []) ? $routes : [ 'users' => 'Users::index', diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index bfe5378b60d0..e298ea723b08 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -51,7 +51,6 @@ protected function getCollector(array $config = [], array $files = [], $moduleCo $routerConfig = new \Config\Routing(); $routerConfig->defaultNamespace = '\\'; - $routerConfig->modulePath = 'Config/Routes.php'; return (new RouteCollection($loader, $moduleConfig, $routerConfig))->setHTTPVerb('get'); } diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php index 9cf8c90bb9a7..35a5abc939ea 100644 --- a/tests/system/Router/RouterTest.php +++ b/tests/system/Router/RouterTest.php @@ -39,7 +39,6 @@ protected function setUp(): void $routerConfig = new \Config\Routing(); $routerConfig->defaultNamespace = '\\'; - $routerConfig->modulePath = 'Config/Routes.php'; $this->collection = new RouteCollection(Services::locator(), $moduleConfig, $routerConfig); diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 74c59205ddb4..7352347b821a 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -105,7 +105,6 @@ Changes characters like ``(``, ``)``, etc., CodeIgniter didn't work. Since v4.4.0, this restriction has been removed. - **Config:** Routing settings have been moved to ``Config\Routing`` config file. -- The default location for new projects Routes.php file has been moved to `app/Routes.php`. This can be modified in the `Config/Routing.php` file. Deprecations ************ diff --git a/user_guide_src/source/general/modules.rst b/user_guide_src/source/general/modules.rst index 40831cb76121..2298c32a986f 100644 --- a/user_guide_src/source/general/modules.rst +++ b/user_guide_src/source/general/modules.rst @@ -154,8 +154,6 @@ the **Modules** config file, described above. When working with modules, it can be a problem if the routes in the application contain wildcards. In that case, see :ref:`routing-priority`. -By default, route files are named **Routes.php** and are located in the root directory of the module. You can change this by setting the ``$modulePath`` variable in the **Routing** config file to path to the file, relative to the module's root directory. For example, if you wanted to put your routes in a file named **Routes.php** in the module's ``Config`` directory, you would set the ``$modulePath`` variable to ``Config/Routes.php``. - Filters ======= diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 4e2ec61c5f7d..47dbb1a4a0ef 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -69,7 +69,6 @@ To clean up the routing system, the following changes were made: - New ``app/Config/Routing.php`` file that holds the settings that used to be in the Routes file. - The ``app/Config/Routes.php`` file was simplified so that it only contains the routes without settings and verbiage to clutter the file. - The ``app/Config/Routes.php`` file was moved to ``app/Routes.php`` to make it easier to find. When upgrading, you can change the ``app/Config/Routing.php` file, ``$routeFiles`` property to point to the old location if you prefer. - - Any module ``Routes.php`` files are expected to be in the namespace's root directory now. To adjust this to match the functionality of existing projects, you can cahnge the ``$modulePath`` property in ``app/Config/Routing.php`` to ``'Config/Routes.php'``. - The environment-specific routes files are no longer loaded automatically. To load those, you must add them to the ``$routeFiles`` property in ``app/Config/Routing.php``. Config From b2ae0cc4e67109ac382494a6d80b6ab2fe04859b Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 3 Apr 2023 23:20:14 -0500 Subject: [PATCH 103/485] Code quality improvements --- app/Config/Routes.php | 4 +++- tests/system/Commands/Utilities/Routes/FilterFinderTest.php | 2 +- tests/system/Router/AutoRouterImprovedTest.php | 3 ++- tests/system/Router/RouteCollectionReverseRouteTest.php | 3 ++- tests/system/Router/RouteCollectionTest.php | 3 ++- tests/system/Router/RouterTest.php | 5 +++-- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 4fb22a2e358c..fc4914a6923b 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1,6 +1,8 @@ get('/', 'Home::index'); diff --git a/tests/system/Commands/Utilities/Routes/FilterFinderTest.php b/tests/system/Commands/Utilities/Routes/FilterFinderTest.php index 91d73153dc2d..9829e36a490a 100644 --- a/tests/system/Commands/Utilities/Routes/FilterFinderTest.php +++ b/tests/system/Commands/Utilities/Routes/FilterFinderTest.php @@ -11,7 +11,6 @@ namespace CodeIgniter\Commands\Utilities\Routes; -use Config\Routing; use CodeIgniter\Config\Services; use CodeIgniter\Filters\CSRF; use CodeIgniter\Filters\DebugToolbar; @@ -26,6 +25,7 @@ use CodeIgniter\Test\ConfigFromArrayTrait; use Config\Filters as FiltersConfig; use Config\Modules; +use Config\Routing; /** * @internal diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index a460cfdfd56c..d6560e1eb9e6 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -19,6 +19,7 @@ use CodeIgniter\Router\Controllers\Mycontroller; use CodeIgniter\Test\CIUnitTestCase; use Config\Modules; +use Config\Routing; /** * @internal @@ -35,7 +36,7 @@ protected function setUp(): void $moduleConfig = new Modules(); $moduleConfig->enabled = false; - $this->collection = new RouteCollection(Services::locator(), $moduleConfig, new \Config\Routing()); + $this->collection = new RouteCollection(Services::locator(), $moduleConfig, new Routing()); } private function createNewAutoRouter(string $httpVerb = 'get'): AutoRouterImproved diff --git a/tests/system/Router/RouteCollectionReverseRouteTest.php b/tests/system/Router/RouteCollectionReverseRouteTest.php index a5ac40e47cfa..12350edbb8ff 100644 --- a/tests/system/Router/RouteCollectionReverseRouteTest.php +++ b/tests/system/Router/RouteCollectionReverseRouteTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Router\Exceptions\RouterException; use CodeIgniter\Test\CIUnitTestCase; use Config\Modules; +use Config\Routing; use Generator; /** @@ -49,7 +50,7 @@ protected function getCollector(array $config = [], array $files = [], $moduleCo $moduleConfig->enabled = false; } - return (new RouteCollection($loader, $moduleConfig, new \Config\Routing()))->setHTTPVerb('get'); + return (new RouteCollection($loader, $moduleConfig, new Routing()))->setHTTPVerb('get'); } public function testReverseRoutingFindsSimpleMatch() diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index e298ea723b08..be5ecd4f2287 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Test\CIUnitTestCase; use Config\Modules; +use Config\Routing; use Tests\Support\Controllers\Hello; /** @@ -49,7 +50,7 @@ protected function getCollector(array $config = [], array $files = [], $moduleCo $moduleConfig->enabled = false; } - $routerConfig = new \Config\Routing(); + $routerConfig = new Routing(); $routerConfig->defaultNamespace = '\\'; return (new RouteCollection($loader, $moduleConfig, $routerConfig))->setHTTPVerb('get'); diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php index 35a5abc939ea..2898f154716e 100644 --- a/tests/system/Router/RouterTest.php +++ b/tests/system/Router/RouterTest.php @@ -18,6 +18,7 @@ use CodeIgniter\Router\Exceptions\RouterException; use CodeIgniter\Test\CIUnitTestCase; use Config\Modules; +use Config\Routing; use Tests\Support\Filters\Customfilter; /** @@ -37,10 +38,10 @@ protected function setUp(): void $moduleConfig = new Modules(); $moduleConfig->enabled = false; - $routerConfig = new \Config\Routing(); + $routerConfig = new Routing(); $routerConfig->defaultNamespace = '\\'; - $this->collection = new RouteCollection(Services::locator(), $moduleConfig, $routerConfig); + $this->collection = new RouteCollection(Services::locator(), $moduleConfig, $routerConfig); $routes = [ '/' => 'Home::index', From 79bc6617a33aca2c260ece35f8e5033265b6a40c Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Wed, 5 Apr 2023 09:07:05 -0500 Subject: [PATCH 104/485] Updating doc code samples and CodeIgniter test --- tests/system/CodeIgniterTest.php | 3 ++- user_guide_src/source/incoming/routing/049.php | 2 +- user_guide_src/source/incoming/routing/050.php | 2 +- user_guide_src/source/incoming/routing/051.php | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index a7f5b9bd1908..f01986553e83 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -22,6 +22,7 @@ use Config\Cache; use Config\Filters as FiltersConfig; use Config\Modules; +use Config\Routing; use Tests\Support\Filters\Customfilter; /** @@ -165,7 +166,7 @@ public function testRun404OverrideByClosure() $_SERVER['argc'] = 2; // Inject mock router. - $routes = new RouteCollection(Services::locator(), new Modules()); + $routes = new RouteCollection(Services::locator(), new Modules(), new Routing()); $routes->setAutoRoute(false); $routes->set404Override(static function () { echo '404 Override by Closure.'; diff --git a/user_guide_src/source/incoming/routing/049.php b/user_guide_src/source/incoming/routing/049.php index 57bee07fcb67..dd276e95316f 100644 --- a/user_guide_src/source/incoming/routing/049.php +++ b/user_guide_src/source/incoming/routing/049.php @@ -1,7 +1,7 @@ setTranslateURIDashes(true); diff --git a/user_guide_src/source/incoming/routing/050.php b/user_guide_src/source/incoming/routing/050.php index 8133cf77b7f1..dc24bfe41416 100644 --- a/user_guide_src/source/incoming/routing/050.php +++ b/user_guide_src/source/incoming/routing/050.php @@ -1,7 +1,7 @@ setAutoRoute(false); diff --git a/user_guide_src/source/incoming/routing/051.php b/user_guide_src/source/incoming/routing/051.php index 098d85178575..125308e20163 100644 --- a/user_guide_src/source/incoming/routing/051.php +++ b/user_guide_src/source/incoming/routing/051.php @@ -1,7 +1,7 @@ set404Override('App\Errors::show404'); From f673df4901273886c427b475ae09ce2e3c90a3a3 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Tue, 4 Apr 2023 15:45:47 -0500 Subject: [PATCH 105/485] Apply suggestions from code review Co-authored-by: Michal Sniatala --- user_guide_src/source/incoming/routing.rst | 12 ++++++------ user_guide_src/source/installation/upgrade_440.rst | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index cee4c6ba672a..64233c05f598 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -22,7 +22,7 @@ First, let's look at Defined Route Routing. If you want to use Auto Routing, see Setting Routing Rules ********************* -Routing rules are defined in the **app/Routes.php** file. In it you'll see that +Routing rules are defined in the **app/Config/Routes.php** file. In it you'll see that it creates an instance of the RouteCollection class (``$routes``) that permits you to specify your own routing criteria. Routes can be specified using placeholders or Regular Expressions. @@ -408,7 +408,7 @@ The value for the filter can be a string or an array of strings: See :doc:`Controller Filters ` for more information on setting up filters. -.. Warning:: If you set filters to routes in **app/Routes.php** +.. 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 (Legacy). When :ref:`auto-routing-legacy` is enabled, it may be possible that a controller can be accessed via a different URL than the configured route, @@ -677,7 +677,7 @@ See :ref:`Auto Routing in Controllers ` for mo Configuration Options ===================== -These options are available at the top of **app/Routes.php**. +These options are available at the top of **app/Config/Routes.php**. Default Controller ------------------ @@ -720,7 +720,7 @@ Auto Routing (Legacy) Auto Routing (Legacy) is a routing system from CodeIgniter 3. It can automatically route HTTP requests based on conventions and execute the corresponding controller methods. -It is recommended that all routes are defined in the **app/Routes.php** file, +It is recommended that all routes are defined in the **app/Config/Routes.php** file, or to use :ref:`auto-routing-improved`, .. warning:: To prevent misconfiguration and miscoding, we recommend that you do not use @@ -734,7 +734,7 @@ Enable Auto Routing (Legacy) Since v4.2.0, the auto-routing is disabled by default. -To use it, you need to change the setting ``setAutoRoute()`` option to true in **app/Routes.php**:: +To use it, you need to change the setting ``$autoRoute`` option to true in **app/Config/Routing.php**:: $routes->setAutoRoute(true); @@ -761,7 +761,7 @@ See :ref:`Auto Routing (Legacy) in Controllers ` Configuration Options (Legacy) ============================== -These options are available at the top of **app/Routes.php**. +These options are available at the top of **app/Config/Routes.php**. Default Controller (Legacy) --------------------------- diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 47dbb1a4a0ef..b400aaf23e5d 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -68,7 +68,6 @@ Routing To clean up the routing system, the following changes were made: - New ``app/Config/Routing.php`` file that holds the settings that used to be in the Routes file. - The ``app/Config/Routes.php`` file was simplified so that it only contains the routes without settings and verbiage to clutter the file. - - The ``app/Config/Routes.php`` file was moved to ``app/Routes.php`` to make it easier to find. When upgrading, you can change the ``app/Config/Routing.php` file, ``$routeFiles`` property to point to the old location if you prefer. - The environment-specific routes files are no longer loaded automatically. To load those, you must add them to the ``$routeFiles`` property in ``app/Config/Routing.php``. Config From 56eb7020fa63edbe30f92bbd27a91e7e6fc17981 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Tue, 4 Apr 2023 23:03:59 -0500 Subject: [PATCH 106/485] Apply suggestions from code review Co-authored-by: kenjis --- user_guide_src/source/changelogs/v4.3.3.rst | 12 ------------ user_guide_src/source/incoming/routing.rst | 4 ++-- user_guide_src/source/tutorial/static_pages.rst | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.3.3.rst b/user_guide_src/source/changelogs/v4.3.3.rst index 1430cf767aea..cd9d3fc31a40 100644 --- a/user_guide_src/source/changelogs/v4.3.3.rst +++ b/user_guide_src/source/changelogs/v4.3.3.rst @@ -16,18 +16,6 @@ SECURITY - **Text Helper:** The :php:func:`random_string()` type **alpha**, **alnum**, **numeric** and **nozero** are now cryptographically secure. -BREAKING -******** - -Message Changes -*************** - -Changes -******* - -Deprecations -************ - Bugs Fixed ********** diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 64233c05f598..c1bcb07341d6 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -584,7 +584,7 @@ When no defined route is found that matches the URI, the system will attempt to controllers and methods when Auto Routing is enabled. You can disable this automatic matching, and restrict routes -to only those defined by you, by setting the ``$autoRoute`` option to false: +to only those defined by you, by setting the ``$autoRoute`` property to false: .. literalinclude:: routing/050.php @@ -600,7 +600,7 @@ a valid class/method pair, just like you would show in any route, or a Closure: .. literalinclude:: routing/051.php -Using the ``set404Override`` method within the routes file, you can use closures. Defining the override in the Routing file is restricted to class/method pairs. +Using the ``$override404`` property within the routing config file, you can use closures. Defining the override in the Routing file is restricted to class/method pairs. .. note:: The ``set404Override()`` method does not change the Response status code to ``404``. If you don't set the status code in the controller you set, the default status code ``200`` diff --git a/user_guide_src/source/tutorial/static_pages.rst b/user_guide_src/source/tutorial/static_pages.rst index 37c6121129de..d39224f3b018 100644 --- a/user_guide_src/source/tutorial/static_pages.rst +++ b/user_guide_src/source/tutorial/static_pages.rst @@ -136,7 +136,7 @@ We have made the controller. The next thing is to set routing rules. Routing associates a URI with a controller's method. Let's do that. Open the routing file located at -**app/Routes.php**. +**app/Config/Routes.php**. The only line there to start with should be: From 7a0d1e4e2177f14ff997b09eb560f0aca1a76a59 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Wed, 5 Apr 2023 08:43:42 -0500 Subject: [PATCH 107/485] Apply suggestions from code review --- system/Router/RouteCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 9962bb23e11c..0ae204f140be 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -273,7 +273,7 @@ public function __construct(FileLocator $locator, Modules $moduleConfig, Routing * * @return $this */ - public function loadRoutes(string $routesFile = APPPATH . 'Routes.php') + public function loadRoutes(string $routesFile = APPPATH . 'Config/Routes.php') { if ($this->didDiscover) { return $this; From 35f9a1dda30daaeab6b7526beeb27c19cea0cfbc Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Apr 2023 09:33:56 +0900 Subject: [PATCH 108/485] test: update failed tests --- tests/system/CommonFunctionsTest.php | 10 ++++++---- tests/system/HTTP/RedirectResponseTest.php | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index cf0555fba3d8..92c853f4cf2d 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -32,6 +32,7 @@ use Config\Cookie; use Config\Logger; use Config\Modules; +use Config\Routing; use Config\Services; use Kint; use RuntimeException; @@ -124,7 +125,8 @@ public function testRedirectReturnsRedirectResponse() $response = $this->createMock(Response::class); $routes = new RouteCollection( Services::locator(), - new Modules() + new Modules(), + new Routing() ); Services::injectMock('response', $response); Services::injectMock('routes', $routes); @@ -389,7 +391,7 @@ public function testOldInput() $this->config = new App(); $this->config->baseURL = 'http://example.com/'; - $this->routes = new RouteCollection(Services::locator(), new Modules()); + $this->routes = new RouteCollection(Services::locator(), new Modules(), new Routing()); Services::injectMock('routes', $this->routes); $this->request = new MockIncomingRequest($this->config, new URI('http://example.com'), null, new UserAgent()); @@ -424,7 +426,7 @@ public function testOldInputSerializeData() $this->config = new App(); $this->config->baseURL = 'http://example.com/'; - $this->routes = new RouteCollection(Services::locator(), new Modules()); + $this->routes = new RouteCollection(Services::locator(), new Modules(), new Routing()); Services::injectMock('routes', $this->routes); $this->request = new MockIncomingRequest($this->config, new URI('http://example.com'), null, new UserAgent()); @@ -459,7 +461,7 @@ public function testOldInputArray() $this->config = new App(); $this->config->baseURL = 'http://example.com/'; - $this->routes = new RouteCollection(Services::locator(), new Modules()); + $this->routes = new RouteCollection(Services::locator(), new Modules(), new Routing()); Services::injectMock('routes', $this->routes); $this->request = new MockIncomingRequest($this->config, new URI('http://example.com'), null, new UserAgent()); diff --git a/tests/system/HTTP/RedirectResponseTest.php b/tests/system/HTTP/RedirectResponseTest.php index 65fc0e62d03c..22c3c719b716 100644 --- a/tests/system/HTTP/RedirectResponseTest.php +++ b/tests/system/HTTP/RedirectResponseTest.php @@ -19,6 +19,7 @@ use CodeIgniter\Validation\Validation; use Config\App; use Config\Modules; +use Config\Routing; use Config\Services; /** @@ -47,7 +48,7 @@ protected function setUp(): void $this->config = new App(); $this->config->baseURL = 'http://example.com/'; - $this->routes = new RouteCollection(Services::locator(), new Modules()); + $this->routes = new RouteCollection(Services::locator(), new Modules(), new Routing()); Services::injectMock('routes', $this->routes); $this->request = new MockIncomingRequest( From d74967f9452882597e5bc2ec5aecac7abd12d67e Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Apr 2023 09:34:29 +0900 Subject: [PATCH 109/485] test: fix incorrect tests --- tests/system/RESTful/ResourceControllerTest.php | 2 +- tests/system/RESTful/ResourcePresenterTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/RESTful/ResourceControllerTest.php b/tests/system/RESTful/ResourceControllerTest.php index fe1d0ac6f781..c96cebc8ebcb 100644 --- a/tests/system/RESTful/ResourceControllerTest.php +++ b/tests/system/RESTful/ResourceControllerTest.php @@ -66,7 +66,7 @@ private function createCodeigniter(): void // Inject mock router. $this->routes = Services::routes(); - $this->routes->resource('work', ['controller' => Worker::class]); + $this->routes->resource('work', ['controller' => '\\' . Worker::class]); Services::injectMock('routes', $this->routes); $config = new App(); diff --git a/tests/system/RESTful/ResourcePresenterTest.php b/tests/system/RESTful/ResourcePresenterTest.php index a430b527a580..8c72d4d6ffb8 100644 --- a/tests/system/RESTful/ResourcePresenterTest.php +++ b/tests/system/RESTful/ResourcePresenterTest.php @@ -60,7 +60,7 @@ private function createCodeigniter(): void // Inject mock router. $this->routes = Services::routes(); - $this->routes->presenter('work', ['controller' => Worker2::class]); + $this->routes->presenter('work', ['controller' => '\\' . Worker2::class]); Services::injectMock('routes', $this->routes); $config = new App(); From 1aba5dbaf798d802735d99adcfcc03185d7f55ee Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Apr 2023 09:34:54 +0900 Subject: [PATCH 110/485] docs: to pass cs-fix --- user_guide_src/source/incoming/routing/045.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/incoming/routing/045.php b/user_guide_src/source/incoming/routing/045.php index f20d34a40681..08ae4e4f2bb7 100644 --- a/user_guide_src/source/incoming/routing/045.php +++ b/user_guide_src/source/incoming/routing/045.php @@ -1,7 +1,7 @@ get('users', 'Users::index'); From 2eb2ee55252b0fe6c8e1301966108f1f0c2b4579 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Apr 2023 09:51:42 +0900 Subject: [PATCH 111/485] test: extract method --- tests/system/CommonFunctionsTest.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 92c853f4cf2d..a40e4cd8d438 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -118,17 +118,19 @@ public function testEnvBooleans() $this->assertNull(env('p4')); } + private function createRouteCollection(): RouteCollection + { + return new RouteCollection(Services::locator(), new Modules(), new Routing()); + } + public function testRedirectReturnsRedirectResponse() { $_SERVER['REQUEST_METHOD'] = 'GET'; $response = $this->createMock(Response::class); - $routes = new RouteCollection( - Services::locator(), - new Modules(), - new Routing() - ); Services::injectMock('response', $response); + + $routes = $this->createRouteCollection(); Services::injectMock('routes', $routes); $routes->add('home/base', 'Controller::index', ['as' => 'base']); @@ -391,7 +393,7 @@ public function testOldInput() $this->config = new App(); $this->config->baseURL = 'http://example.com/'; - $this->routes = new RouteCollection(Services::locator(), new Modules(), new Routing()); + $this->routes = $this->createRouteCollection(); Services::injectMock('routes', $this->routes); $this->request = new MockIncomingRequest($this->config, new URI('http://example.com'), null, new UserAgent()); @@ -426,7 +428,7 @@ public function testOldInputSerializeData() $this->config = new App(); $this->config->baseURL = 'http://example.com/'; - $this->routes = new RouteCollection(Services::locator(), new Modules(), new Routing()); + $this->routes = $this->createRouteCollection(); Services::injectMock('routes', $this->routes); $this->request = new MockIncomingRequest($this->config, new URI('http://example.com'), null, new UserAgent()); @@ -461,7 +463,7 @@ public function testOldInputArray() $this->config = new App(); $this->config->baseURL = 'http://example.com/'; - $this->routes = new RouteCollection(Services::locator(), new Modules(), new Routing()); + $this->routes = $this->createRouteCollection(); Services::injectMock('routes', $this->routes); $this->request = new MockIncomingRequest($this->config, new URI('http://example.com'), null, new UserAgent()); From a47b8f1bb54940cc0dd90d0aea2479d969c3f396 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Apr 2023 10:06:01 +0900 Subject: [PATCH 112/485] docs: improve sample code class files --- user_guide_src/source/incoming/routing/045.php | 7 ++++++- user_guide_src/source/incoming/routing/049.php | 7 ++++++- user_guide_src/source/incoming/routing/050.php | 7 ++++++- user_guide_src/source/incoming/routing/051.php | 7 ++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/incoming/routing/045.php b/user_guide_src/source/incoming/routing/045.php index 08ae4e4f2bb7..e9534931dbc8 100644 --- a/user_guide_src/source/incoming/routing/045.php +++ b/user_guide_src/source/incoming/routing/045.php @@ -1,7 +1,12 @@ get('users', 'Users::index'); diff --git a/user_guide_src/source/incoming/routing/049.php b/user_guide_src/source/incoming/routing/049.php index dd276e95316f..9f53bee87476 100644 --- a/user_guide_src/source/incoming/routing/049.php +++ b/user_guide_src/source/incoming/routing/049.php @@ -1,7 +1,12 @@ setTranslateURIDashes(true); diff --git a/user_guide_src/source/incoming/routing/050.php b/user_guide_src/source/incoming/routing/050.php index dc24bfe41416..6f5446f5a654 100644 --- a/user_guide_src/source/incoming/routing/050.php +++ b/user_guide_src/source/incoming/routing/050.php @@ -1,7 +1,12 @@ setAutoRoute(false); diff --git a/user_guide_src/source/incoming/routing/051.php b/user_guide_src/source/incoming/routing/051.php index 125308e20163..bced5d53147a 100644 --- a/user_guide_src/source/incoming/routing/051.php +++ b/user_guide_src/source/incoming/routing/051.php @@ -1,7 +1,12 @@ set404Override('App\Errors::show404'); From 329d90beee0a0c3489b7437737be945d7cb8761d Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Apr 2023 10:10:07 +0900 Subject: [PATCH 113/485] docs: add note for new config file --- user_guide_src/source/incoming/routing.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index c1bcb07341d6..066009beda65 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -547,6 +547,10 @@ 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 in **app/Config/Routing.php**. +.. note:: The config file **app/Config/Routing.php** has been added since v4.4.0. + In previous versions, the setter methods were used in **app/Config/Routes.php** + to change settings. + .. _routing-default-namespace: Default Namespace From 5243ba32141f8bacfd4a7e3c0014a07083623969 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Apr 2023 10:19:48 +0900 Subject: [PATCH 114/485] docs: add changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 7352347b821a..b8c586d47c3d 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -35,6 +35,8 @@ Interface Changes Method Signature Changes ======================== +- The third parameter ``Routing $routing`` has been added to ``RouteCollection::__construct()``. + Enhancements ************ @@ -97,6 +99,7 @@ Changes ******* - **Config:** The deprecated Cookie items in **app/Config/App.php** has been removed. +- **Config:** Routing settings have been moved to **app/Config/Routing.php** config file. - **DownloadResponse:** When generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. - **Autoloader:** Before v4.4.0, CodeIgniter autoloader did not allow special characters that are illegal in filenames on certain operating systems. @@ -104,7 +107,6 @@ Changes So if you installed CodeIgniter under the folder that contains the special characters like ``(``, ``)``, etc., CodeIgniter didn't work. Since v4.4.0, this restriction has been removed. -- **Config:** Routing settings have been moved to ``Config\Routing`` config file. Deprecations ************ From e5e6766cae1b33e3c5c65d780cdda2a1934c776f Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Apr 2023 10:37:36 +0900 Subject: [PATCH 115/485] docs: update ugrade_440 --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + .../source/installation/upgrade_440.rst | 26 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index b8c586d47c3d..40f45d72e010 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -100,6 +100,7 @@ Changes - **Config:** The deprecated Cookie items in **app/Config/App.php** has been removed. - **Config:** Routing settings have been moved to **app/Config/Routing.php** config file. + See :ref:`Upgrading Guide `. - **DownloadResponse:** When generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. - **Autoloader:** Before v4.4.0, CodeIgniter autoloader did not allow special characters that are illegal in filenames on certain operating systems. diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index b400aaf23e5d..102308545b51 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -34,6 +34,24 @@ Mandatory File Changes Config Files ============ +.. _upgrade-440-config-routing: + +app/Config/Routing.php +---------------------- + +To clean up the routing system, the following changes were made: + +- New **app/Config/Routing.php** file that holds the settings that used to be in the Routes file. +- The **app/Config/Routes.php** file was simplified so that it only contains the routes without settings and verbiage to clutter the file. +- The environment-specific routes files are no longer loaded automatically. + +So you need to do: + +1. Copy **app/Config/Routing.php** from the new framework to your **app/Config** + directory, and configure it. +2. Remove all settings in **app/Config/Routes.php** that are no longer needed. +3. If you use the environment-specific routes files, add them to the ``$routeFiles`` property in **app/Config/Routing.php**. + app/Config/Cookie.php --------------------- @@ -62,14 +80,6 @@ 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: -Routing -------- - -To clean up the routing system, the following changes were made: - - New ``app/Config/Routing.php`` file that holds the settings that used to be in the Routes file. - - The ``app/Config/Routes.php`` file was simplified so that it only contains the routes without settings and verbiage to clutter the file. - - The environment-specific routes files are no longer loaded automatically. To load those, you must add them to the ``$routeFiles`` property in ``app/Config/Routing.php``. - Config ------ From 48e3864a07c34f9e5f5f96d5245169b6dd9ec1b8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Apr 2023 10:57:34 +0900 Subject: [PATCH 116/485] docs: add instruction to upgrade --- user_guide_src/source/installation/upgrade_440.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 102308545b51..6200fa79ae22 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -65,6 +65,10 @@ The Cookie config items in **app/Config/App.php** are no longer used. Breaking Enhancements ********************* +- The method signature of ``RouteCollection::__construct()`` has been changed. + The third parameter ``Routing $routing`` has been added. Extending classes + should likewise add the parameter so as not to break LSP. + Project Files ************* From 5405ba4a662b286cb10829241cc2b679351776fc Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Fri, 7 Apr 2023 13:06:23 +0200 Subject: [PATCH 117/485] Fixed typos --- system/View/Table.php | 2 +- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/View/Table.php b/system/View/Table.php index 223d16db71e3..fb98248c9e6e 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -242,7 +242,7 @@ public function addRow() * If a row has a key which does not exist in heading, it will be filtered out * If a row does not have a key which exists in heading, the field will stay empty * - * @return Table + * @return $this */ public function setSyncRowsWithHeading(bool $orderByKey) { diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 6e38883448ad..b90a9c75fc9f 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -87,7 +87,7 @@ Others See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. -- **Table:** Addedd ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with heading. See :ref:`table-sync-rows-with-headings` for details. +- **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with heading. See :ref:`table-sync-rows-with-headings` for details. Message Changes *************** From 85fdd59d26426a653fc36fd76c443b226ec44e5c Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Feb 2023 11:25:36 +0900 Subject: [PATCH 118/485] chore: update test-phpcpd.yml --- .github/workflows/test-phpcpd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-phpcpd.yml b/.github/workflows/test-phpcpd.yml index fe75c0929193..31dcdb232749 100644 --- a/.github/workflows/test-phpcpd.yml +++ b/.github/workflows/test-phpcpd.yml @@ -54,4 +54,5 @@ jobs: --exclude system/Database/MySQLi/Builder.php --exclude system/Database/OCI8/Builder.php --exclude system/Database/Postgre/Builder.php + --exclude system/Debug/Exceptions.php -- app/ public/ system/ From 3e1d653b206046aaa05d445683416325eaed8e90 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Feb 2023 11:29:41 +0900 Subject: [PATCH 119/485] feat: add Debug\ExceptionHandler --- app/Config/Exceptions.php | 27 +++ system/Debug/BaseExceptionHandler.php | 233 ++++++++++++++++++++ system/Debug/ExceptionHandler.php | 149 +++++++++++++ system/Debug/ExceptionHandlerInterface.php | 32 +++ tests/system/Debug/ExceptionHandlerTest.php | 143 ++++++++++++ 5 files changed, 584 insertions(+) create mode 100644 system/Debug/BaseExceptionHandler.php create mode 100644 system/Debug/ExceptionHandler.php create mode 100644 system/Debug/ExceptionHandlerInterface.php create mode 100644 tests/system/Debug/ExceptionHandlerTest.php diff --git a/app/Config/Exceptions.php b/app/Config/Exceptions.php index bf3a1b964aeb..4173dcdd1c70 100644 --- a/app/Config/Exceptions.php +++ b/app/Config/Exceptions.php @@ -3,7 +3,10 @@ namespace Config; use CodeIgniter\Config\BaseConfig; +use CodeIgniter\Debug\ExceptionHandler; +use CodeIgniter\Debug\ExceptionHandlerInterface; use Psr\Log\LogLevel; +use Throwable; /** * Setup how the exception handler works. @@ -74,4 +77,28 @@ class Exceptions extends BaseConfig * to capture logging the deprecations. */ public string $deprecationLogLevel = LogLevel::WARNING; + + /* + * DEFINE THE HANDLERS USED + * -------------------------------------------------------------------------- + * Given the HTTP status code, returns exception handler that + * should be used to deal with this error. By default, it will run CodeIgniter's + * default handler and display the error information in the expected format + * for CLI, HTTP, or AJAX requests, as determined by is_cli() and the expected + * response format. + * + * Custom handlers can be returned if you want to handle one or more specific + * error codes yourself like: + * + * if (in_array($statusCode, [400, 404, 500])) { + * return new \App\Libraries\MyExceptionHandler(); + * } + * if ($exception instanceOf PageNotFoundException) { + * return new \App\Libraries\MyExceptionHandler(); + * } + */ + public function handler(int $statusCode, Throwable $exception): ExceptionHandlerInterface + { + return new ExceptionHandler($this); + } } diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php new file mode 100644 index 000000000000..a4a4c72946b1 --- /dev/null +++ b/system/Debug/BaseExceptionHandler.php @@ -0,0 +1,233 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug; + +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use Config\Exceptions as ExceptionsConfig; +use Throwable; + +/** + * Provides common functions for exception handlers, + * especially around displaying the output. + */ +abstract class BaseExceptionHandler +{ + /** + * Config for debug exceptions. + */ + protected ExceptionsConfig $config; + + /** + * Nesting level of the output buffering mechanism + */ + protected int $obLevel; + + /** + * The path to the directory containing the + * cli and html error view directories. + */ + protected ?string $viewPath = null; + + public function __construct(ExceptionsConfig $config) + { + $this->config = $config; + + $this->obLevel = ob_get_level(); + + if ($this->viewPath === null) { + $this->viewPath = rtrim($this->config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR; + } + } + + /** + * The main entry point into the handler. + * + * @return void + */ + abstract public function handle( + Throwable $exception, + RequestInterface $request, + ResponseInterface $response, + int $statusCode, + int $exitCode + ); + + /** + * Gathers the variables that will be made available to the view. + */ + protected function collectVars(Throwable $exception, int $statusCode): array + { + $trace = $exception->getTrace(); + + if ($this->config->sensitiveDataInTrace !== []) { + $this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace); + } + + return [ + 'title' => get_class($exception), + 'type' => get_class($exception), + 'code' => $statusCode, + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => $trace, + ]; + } + + /** + * Mask sensitive data in the trace. + * + * @param array|object $trace + */ + protected function maskSensitiveData(&$trace, array $keysToMask, string $path = '') + { + foreach ($keysToMask as $keyToMask) { + $explode = explode('/', $keyToMask); + $index = end($explode); + + if (strpos(strrev($path . '/' . $index), strrev($keyToMask)) === 0) { + if (is_array($trace) && array_key_exists($index, $trace)) { + $trace[$index] = '******************'; + } elseif (is_object($trace) && property_exists($trace, $index) && isset($trace->{$index})) { + $trace->{$index} = '******************'; + } + } + } + + if (is_object($trace)) { + $trace = get_object_vars($trace); + } + + if (is_array($trace)) { + foreach ($trace as $pathKey => $subarray) { + $this->maskSensitiveData($subarray, $keysToMask, $path . '/' . $pathKey); + } + } + } + + /** + * Describes memory usage in real-world units. Intended for use + * with memory_get_usage, etc. + * + * @used-by app/Views/errors/html/error_exception.php + */ + protected static function describeMemory(int $bytes): string + { + helper('number'); + + return number_to_size($bytes, 2); + } + + /** + * Creates a syntax-highlighted version of a PHP file. + * + * @used-by app/Views/errors/html/error_exception.php + * + * @return bool|string + */ + protected static function highlightFile(string $file, int $lineNumber, int $lines = 15) + { + if (empty($file) || ! is_readable($file)) { + return false; + } + + // Set our highlight colors: + if (function_exists('ini_set')) { + ini_set('highlight.comment', '#767a7e; font-style: italic'); + ini_set('highlight.default', '#c7c7c7'); + ini_set('highlight.html', '#06B'); + ini_set('highlight.keyword', '#f1ce61;'); + ini_set('highlight.string', '#869d6a'); + } + + try { + $source = file_get_contents($file); + } catch (Throwable $e) { + return false; + } + + $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 = 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); + + // Used to format the line number in the source + $format = '% ' . strlen((string) ($start + $lines)) . 'd'; + + $out = ''; + // Because the highlighting may have an uneven number + // of open and close span tags on one line, we need + // to ensure we can close them all to get the lines + // showing correctly. + $spans = 1; + + foreach ($source as $n => $row) { + $spans += substr_count($row, ']+>#', $row, $tags); + + $out .= sprintf( + "{$format} %s\n%s", + $n + $start + 1, + strip_tags($row), + implode('', $tags[0]) + ); + } else { + $out .= sprintf('' . $format . ' %s', $n + $start + 1, $row) . "\n"; + } + } + + if ($spans > 0) { + $out .= str_repeat('', $spans); + } + + return '
' . $out . '
'; + } + + /** + * Given an exception and status code will display the error to the client. + * + * @param string|null $viewFile + */ + protected function render(Throwable $exception, int $statusCode, $viewFile = null): void + { + if (empty($viewFile) || ! is_file($viewFile)) { + echo 'The error view files were not found. Cannot render exception trace.'; + + exit(1); + } + + if (ob_get_level() > $this->obLevel + 1) { + ob_end_clean(); + } + + echo(function () use ($exception, $statusCode, $viewFile): string { + $vars = $this->collectVars($exception, $statusCode); + extract($vars, EXTR_SKIP); + + // CLI error views output to STDERR/STDOUT, so ob_start() does not work. + ob_start(); + include $viewFile; + + return ob_get_clean(); + })(); + } +} diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php new file mode 100644 index 000000000000..7c1f1090edf8 --- /dev/null +++ b/system/Debug/ExceptionHandler.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug; + +use CodeIgniter\API\ResponseTrait; +use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use Config\Paths; +use Throwable; + +final class ExceptionHandler extends BaseExceptionHandler implements ExceptionHandlerInterface +{ + use ResponseTrait; + + /** + * ResponseTrait needs this. + */ + private ?RequestInterface $request = null; + + /** + * ResponseTrait needs this. + */ + private ?ResponseInterface $response = null; + + /** + * Determines the correct way to display the error. + * + * @return void + */ + public function handle( + Throwable $exception, + RequestInterface $request, + ResponseInterface $response, + int $statusCode, + int $exitCode + ) { + // ResponseTrait needs these properties. + $this->request = $request; + $this->response = $response; + + if ($request instanceof IncomingRequest) { + try { + $response->setStatusCode($statusCode); + } catch (HTTPException $e) { + // Workaround for invalid HTTP status code. + $statusCode = 500; + $response->setStatusCode($statusCode); + } + + if (! headers_sent()) { + header( + sprintf( + 'HTTP/%s %s %s', + $request->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ), + true, + $statusCode + ); + } + + if (strpos($request->getHeaderLine('accept'), 'text/html') === false) { + $data = (ENVIRONMENT === 'development' || ENVIRONMENT === 'testing') + ? $this->collectVars($exception, $statusCode) + : ''; + + $this->respond($data, $statusCode)->send(); + + if (ENVIRONMENT !== 'testing') { + // @codeCoverageIgnoreStart + exit($exitCode); + // @codeCoverageIgnoreEnd + } + + return; + } + } + + // Determine possible directories of error views + $addPath = ($request instanceof IncomingRequest ? 'html' : 'cli') . DIRECTORY_SEPARATOR; + $path = $this->viewPath . $addPath; + $altPath = rtrim((new Paths())->viewDirectory, '\\/ ') + . DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR . $addPath; + + // Determine the views + $view = $this->determineView($exception, $path); + $altView = $this->determineView($exception, $altPath); + + // Check if the view exists + $viewFile = null; + if (is_file($path . $view)) { + $viewFile = $path . $view; + } elseif (is_file($altPath . $altView)) { + $viewFile = $altPath . $altView; + } + + // Displays the HTML or CLI error code. + $this->render($exception, $statusCode, $viewFile); + + if (ENVIRONMENT !== 'testing') { + // @codeCoverageIgnoreStart + exit($exitCode); + // @codeCoverageIgnoreEnd + } + } + + /** + * Determines the view to display based on the exception thrown, + * whether an HTTP or CLI request, etc. + * + * @return string The filename of the view file to use + */ + protected function determineView(Throwable $exception, string $templatePath): string + { + // Production environments should have a custom exception file. + $view = 'production.php'; + + if (str_ireplace(['off', 'none', 'no', 'false', 'null'], '', ini_get('display_errors'))) { + $view = 'error_exception.php'; + } + + // 404 Errors + if ($exception instanceof PageNotFoundException) { + return 'error_404.php'; + } + + $templatePath = rtrim($templatePath, '\\/ ') . DIRECTORY_SEPARATOR; + + // Allow for custom views based upon the status code + if (is_file($templatePath . 'error_' . $exception->getCode() . '.php')) { + return 'error_' . $exception->getCode() . '.php'; + } + + return $view; + } +} diff --git a/system/Debug/ExceptionHandlerInterface.php b/system/Debug/ExceptionHandlerInterface.php new file mode 100644 index 000000000000..29a44c477305 --- /dev/null +++ b/system/Debug/ExceptionHandlerInterface.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\Debug; + +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use Throwable; + +interface ExceptionHandlerInterface +{ + /** + * Determines the correct way to display the error. + * + * @return void + */ + public function handle( + Throwable $exception, + RequestInterface $request, + ResponseInterface $response, + int $statusCode, + int $exitCode + ); +} diff --git a/tests/system/Debug/ExceptionHandlerTest.php b/tests/system/Debug/ExceptionHandlerTest.php new file mode 100644 index 000000000000..f28f44a9b7e4 --- /dev/null +++ b/tests/system/Debug/ExceptionHandlerTest.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug; + +use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\StreamFilterTrait; +use Config\Exceptions as ExceptionsConfig; +use Config\Services; +use RuntimeException; + +/** + * @internal + * + * @group Others + */ +final class ExceptionHandlerTest extends CIUnitTestCase +{ + use StreamFilterTrait; + + private ExceptionHandler $handler; + + protected function setUp(): void + { + parent::setUp(); + + $this->handler = new ExceptionHandler(new ExceptionsConfig()); + } + + public function testDetermineViewsPageNotFoundException(): void + { + $determineView = $this->getPrivateMethodInvoker($this->handler, 'determineView'); + + $exception = PageNotFoundException::forControllerNotFound('Foo', 'bar'); + $templatePath = APPPATH . 'Views/errors/html'; + $viewFile = $determineView($exception, $templatePath); + + $this->assertSame('error_404.php', $viewFile); + } + + public function testDetermineViewsRuntimeException(): void + { + $determineView = $this->getPrivateMethodInvoker($this->handler, 'determineView'); + + $exception = new RuntimeException('Exception'); + $templatePath = APPPATH . 'Views/errors/html'; + $viewFile = $determineView($exception, $templatePath); + + $this->assertSame('error_exception.php', $viewFile); + } + + public function testDetermineViewsRuntimeExceptionCode404(): void + { + $determineView = $this->getPrivateMethodInvoker($this->handler, 'determineView'); + + $exception = new RuntimeException('foo', 404); + $templatePath = APPPATH . 'Views/errors/html'; + $viewFile = $determineView($exception, $templatePath); + + $this->assertSame('error_404.php', $viewFile); + } + + public function testCollectVars(): void + { + $collectVars = $this->getPrivateMethodInvoker($this->handler, 'collectVars'); + + $vars = $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 testHandleWebPageNotFoundExceptionDoNotAcceptHTML(): void + { + $exception = PageNotFoundException::forControllerNotFound('Foo', 'bar'); + + $request = Services::incomingrequest(null, false); + $response = Services::response(null, false); + $response->pretend(); + + ob_start(); + $this->handler->handle($exception, $request, $response, 404, EXIT_ERROR); + $output = ob_get_clean(); + + $json = json_decode($output); + $this->assertSame(PageNotFoundException::class, $json->title); + $this->assertSame(PageNotFoundException::class, $json->type); + $this->assertSame(404, $json->code); + $this->assertSame('Controller or its method is not found: Foo::bar', $json->message); + } + + public function testHandleWebPageNotFoundExceptionAcceptHTML(): void + { + $exception = PageNotFoundException::forControllerNotFound('Foo', 'bar'); + + $request = Services::incomingrequest(null, false); + $request->setHeader('accept', 'text/html'); + $response = Services::response(null, false); + $response->pretend(); + + ob_start(); + $this->handler->handle($exception, $request, $response, 404, EXIT_ERROR); + $output = ob_get_clean(); + + $this->assertStringContainsString('404 - Page Not Found', $output); + } + + public function testHandleCLIPageNotFoundException(): void + { + $exception = PageNotFoundException::forControllerNotFound('Foo', 'bar'); + + $request = Services::clirequest(null, false); + $request->setHeader('accept', 'text/html'); + $response = Services::response(null, false); + $response->pretend(); + + $this->handler->handle($exception, $request, $response, 404, EXIT_ERROR); + + $this->assertStringContainsString( + 'ERROR: 404', + $this->getStreamFilterBuffer() + ); + $this->assertStringContainsString( + 'Controller or its method is not found: Foo::bar', + $this->getStreamFilterBuffer() + ); + + $this->resetStreamFilterBuffer(); + } +} From 0aba74d6d6797723da2a20818581133145dfedd1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Feb 2023 11:30:30 +0900 Subject: [PATCH 120/485] feat: use ExceptionHandler --- system/Debug/Exceptions.php | 42 +++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 07e4ecc29dbd..301ba719513e 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -15,9 +15,8 @@ use CodeIgniter\Exceptions\HasExitCodeInterface; use CodeIgniter\Exceptions\HTTPExceptionInterface; use CodeIgniter\Exceptions\PageNotFoundException; -use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\Exceptions\HTTPException; -use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Config\Exceptions as ExceptionsConfig; use Config\Paths; @@ -37,6 +36,8 @@ class Exceptions * Nesting level of the output buffering mechanism * * @var int + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ public $ob_level; @@ -45,6 +46,8 @@ class Exceptions * cli and html error view directories. * * @var string + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ protected $viewPath; @@ -58,7 +61,7 @@ class Exceptions /** * The request. * - * @var CLIRequest|IncomingRequest + * @var RequestInterface */ protected $request; @@ -72,14 +75,16 @@ class Exceptions private ?Throwable $exceptionCaughtByExceptionHandler = null; /** - * @param CLIRequest|IncomingRequest|null $request + * @param RequestInterface|null $request * * @deprecated The parameter $request and $response are deprecated. No longer used. */ public function __construct(ExceptionsConfig $config, $request, ResponseInterface $response) /** @phpstan-ignore-line */ { + // For backward compatibility $this->ob_level = ob_get_level(); $this->viewPath = rtrim($config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR; + $this->config = $config; // workaround for upgraded users @@ -111,8 +116,6 @@ public function initialize() * Catches any uncaught errors and exceptions, including most Fatal errors * (Yay PHP7!). Will log the error, display it if display_errors is on, * and fire an event that allows custom actions to be taken at this point. - * - * @codeCoverageIgnore */ public function exceptionHandler(Throwable $exception) { @@ -128,6 +131,21 @@ public function exceptionHandler(Throwable $exception) $exception = $prevException; } + if (method_exists($this->config, 'handler')) { + // Use new ExceptionHandler + $handler = $this->config->handler($statusCode, $exception); + $handler->handle( + $exception, + $this->request, + $this->response, + $statusCode, + $exitCode + ); + + return; + } + + // For backward compatibility if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { log_message('critical', "{message}\nin {exFile} on line {exLine}.\n{trace}", [ 'message' => $exception->getMessage(), @@ -221,6 +239,8 @@ public function shutdownHandler() * whether an HTTP or CLI request, etc. * * @return string The path and filename of the view file to use + * + * @deprecated No longer used. Moved to ExceptionHandler. */ protected function determineView(Throwable $exception, string $templatePath): string { @@ -247,6 +267,8 @@ protected function determineView(Throwable $exception, string $templatePath): st /** * Given an exception and status code will display the error to the client. + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ protected function render(Throwable $exception, int $statusCode) { @@ -291,6 +313,8 @@ protected function render(Throwable $exception, int $statusCode) /** * Gathers the variables that will be made available to the view. + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ protected function collectVars(Throwable $exception, int $statusCode): array { @@ -315,6 +339,8 @@ protected function collectVars(Throwable $exception, int $statusCode): array * Mask sensitive data in the trace. * * @param array|object $trace + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ protected function maskSensitiveData(&$trace, array $keysToMask, string $path = '') { @@ -425,6 +451,8 @@ public static function cleanPath(string $file): string /** * Describes memory usage in real-world units. Intended for use * with memory_get_usage, etc. + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ public static function describeMemory(int $bytes): string { @@ -443,6 +471,8 @@ public static function describeMemory(int $bytes): string * Creates a syntax-highlighted version of a PHP file. * * @return bool|string + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ public static function highlightFile(string $file, int $lineNumber, int $lines = 15) { From a3934e921355edf5bd2dc15994999b41dbaef0d2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Feb 2023 11:36:00 +0900 Subject: [PATCH 121/485] refactor: add return type --- system/Debug/ExceptionHandler.php | 4 +--- system/Debug/ExceptionHandlerInterface.php | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php index 7c1f1090edf8..1eaa05aceeaa 100644 --- a/system/Debug/ExceptionHandler.php +++ b/system/Debug/ExceptionHandler.php @@ -36,8 +36,6 @@ final class ExceptionHandler extends BaseExceptionHandler implements ExceptionHa /** * Determines the correct way to display the error. - * - * @return void */ public function handle( Throwable $exception, @@ -45,7 +43,7 @@ public function handle( ResponseInterface $response, int $statusCode, int $exitCode - ) { + ): void { // ResponseTrait needs these properties. $this->request = $request; $this->response = $response; diff --git a/system/Debug/ExceptionHandlerInterface.php b/system/Debug/ExceptionHandlerInterface.php index 29a44c477305..bbfcb6ba70ab 100644 --- a/system/Debug/ExceptionHandlerInterface.php +++ b/system/Debug/ExceptionHandlerInterface.php @@ -19,8 +19,6 @@ interface ExceptionHandlerInterface { /** * Determines the correct way to display the error. - * - * @return void */ public function handle( Throwable $exception, @@ -28,5 +26,5 @@ public function handle( ResponseInterface $response, int $statusCode, int $exitCode - ); + ): void; } From 5be8fe0c37076acbfc730e270ad4b7058781ba68 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Oct 2022 15:38:40 +0900 Subject: [PATCH 122/485] docs: add user guide --- user_guide_src/source/general/errors.rst | 37 ++++++++++++++++++++ user_guide_src/source/general/errors/015.php | 27 ++++++++++++++ user_guide_src/source/general/errors/016.php | 18 ++++++++++ user_guide_src/source/general/errors/017.php | 26 ++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 user_guide_src/source/general/errors/015.php create mode 100644 user_guide_src/source/general/errors/016.php create mode 100644 user_guide_src/source/general/errors/017.php diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst index c46aed6572ce..93a14c774dbc 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -168,3 +168,40 @@ This feature also works with user deprecations: For testing your application you may want to always throw on deprecations. You may configure this by setting the environment variable ``CODEIGNITER_SCREAM_DEPRECATIONS`` to a truthy value. + +.. _custom-exception-handlers: + +Custom Exception Handlers +========================= + +.. versionadded:: 4.4.0 + +If you need more control over how exceptions are displayed you can now define your own handlers and +specify when they apply. + +Defining the New Handler +------------------------ + +The first step is to create a new class which implements ``CodeIgniter\Debug\ExceptionHandlerInterface``. +You can also extend ``CodeIgniter\Debug\BaseExceptionHandler``. +This class includes a number of utility methods that are used by the default exception handler. +The new handler must implement a single method: ``handle()``: + +.. literalinclude:: errors/015.php + +This example defines the minimum amount of code typically needed - display a view and exit with the proper +exit code. However, the ``BaseExceptionHandler`` provides a number of other helper functions and objects. + +Configuring the New Handler +--------------------------- + +Telling CodeIgniter to use your new exception handler class is done in the **app/Config/Exceptions.php** +configuration file's ``handler()`` method: + +.. literalinclude:: errors/016.php + +You can use any logic your application needs to determine whether it should handle the exception, but the +two most common are checking on the HTTP status code or the type of exception. If your class should handle +it then return a new instance of that class: + +.. literalinclude:: errors/017.php diff --git a/user_guide_src/source/general/errors/015.php b/user_guide_src/source/general/errors/015.php new file mode 100644 index 000000000000..ea3a531a6f23 --- /dev/null +++ b/user_guide_src/source/general/errors/015.php @@ -0,0 +1,27 @@ +render($exception, $statusCode, $this->viewPath . "error_{$statusCode}.php"); + + exit($exitCode); + } +} diff --git a/user_guide_src/source/general/errors/016.php b/user_guide_src/source/general/errors/016.php new file mode 100644 index 000000000000..0fb61fa16cac --- /dev/null +++ b/user_guide_src/source/general/errors/016.php @@ -0,0 +1,18 @@ + Date: Wed, 11 Jan 2023 16:56:22 +0900 Subject: [PATCH 123/485] docs: add changelogs/v4.4.0.rst --- user_guide_src/source/changelogs/v4.4.0.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 503922b418e2..f06814ab1677 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -32,6 +32,10 @@ The next segment (``+1``) of the current last segment can be set as before. Interface Changes ================= +.. note:: As long as you have not extended the relevant CodeIgniter core classes + or implemented these interfaces, all these changes are backward compatible + and require no intervention. + Method Signature Changes ======================== @@ -87,6 +91,7 @@ Others See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. +- **Error Handling:** Now you can use :ref:`custom-exception-handlers`. Message Changes *************** @@ -109,6 +114,9 @@ Deprecations ************ - **Entity:** ``Entity::setAttributes()`` is deprecated. Use ``Entity::injectRawData()`` instead. +- **Error Handling:** Many methods and properties in ``CodeIgniter\Debug\Exceptions`` + are deprecated. Because these methods have been moved to ``BaseExceptionHandler`` or + ``ExceptionHandler``. - **Autoloader:** ``Autoloader::sanitizeFilename()`` is deprecated. Bugs Fixed From 6b9251f7bc05b6da30c406f951f96c03f025bf96 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Feb 2023 12:49:56 +0900 Subject: [PATCH 124/485] docs: add upgrading guide --- .../source/installation/upgrade_440.rst | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index f5bc7aef91f2..784d0d63aeaf 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -28,6 +28,19 @@ If your code depends on this bug, fix the segment number. .. literalinclude:: upgrade_440/002.php :lines: 2- +When you extend Exceptions +========================== + +If you are extending ``CodeIgniter\Debug\Exceptions`` and have not overridden +the ``exceptionHandler()`` method, defining the new ``Config\Exceptions::handler()`` +method in your **app/Config/Exceptions.php** will cause the specified Exception +Handler to be executed. + +Your overridden code will no longer be executed, so make any necessary changes +by defining your own exception handler. + +See :ref:`custom-exception-handlers` for the detail. + Mandatory File Changes ********************** @@ -65,7 +78,9 @@ and it is recommended that you merge the updated versions with your application: Config ------ -- @TODO +- app/Config/Exceptions.php + - Added the new method ``handler()`` that define custom Exception Handlers. + See :ref:`custom-exception-handlers`. All Changes =========== From 2476b7ed576c0c8b5c139c7e1c573080e93540f4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 11 Feb 2023 16:52:56 +0900 Subject: [PATCH 125/485] fix: log exception --- system/Debug/Exceptions.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 301ba719513e..2c571f6788f8 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -123,6 +123,15 @@ public function exceptionHandler(Throwable $exception) [$statusCode, $exitCode] = $this->determineCodes($exception); + if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { + log_message('critical', "{message}\nin {exFile} on line {exLine}.\n{trace}", [ + 'message' => $exception->getMessage(), + 'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file + 'exLine' => $exception->getLine(), // {line} refers to THIS line + 'trace' => self::renderBacktrace($exception->getTrace()), + ]); + } + $this->request = Services::request(); $this->response = Services::response(); @@ -146,15 +155,6 @@ public function exceptionHandler(Throwable $exception) } // For backward compatibility - if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { - log_message('critical', "{message}\nin {exFile} on line {exLine}.\n{trace}", [ - 'message' => $exception->getMessage(), - 'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file - 'exLine' => $exception->getLine(), // {line} refers to THIS line - 'trace' => self::renderBacktrace($exception->getTrace()), - ]); - } - if (! is_cli()) { try { $this->response->setStatusCode($statusCode); From 10ed983597d614b3d7117c54109a967dca6c2dec Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 11:23:54 +0900 Subject: [PATCH 126/485] docs: update @var --- system/Debug/Exceptions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 2c571f6788f8..fde1d4b79dcb 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -61,7 +61,7 @@ class Exceptions /** * The request. * - * @var RequestInterface + * @var RequestInterface|null */ protected $request; From 07a519496ddd0f06e0da2e8f8df0efbc1f4f3e6f Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 11:24:16 +0900 Subject: [PATCH 127/485] style: fix coding style --- system/Debug/Exceptions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index fde1d4b79dcb..9d2ada166aee 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -85,7 +85,7 @@ public function __construct(ExceptionsConfig $config, $request, ResponseInterfac $this->ob_level = ob_get_level(); $this->viewPath = rtrim($config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR; - $this->config = $config; + $this->config = $config; // workaround for upgraded users // This causes "Deprecated: Creation of dynamic property" in PHP 8.2. From 524e880f5b67a2857ee17c383fdf0143ddadb85e Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sat, 8 Apr 2023 09:09:08 +0200 Subject: [PATCH 128/485] Minor updates --- system/View/Table.php | 2 +- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/View/Table.php b/system/View/Table.php index fb98248c9e6e..b66a18c338e6 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -86,7 +86,7 @@ class Table /** * Order each inserted row by heading keys */ - public bool $syncRowsWithHeading = false; + private bool $syncRowsWithHeading = false; /** * Set the template from the table config file if it exists diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index b90a9c75fc9f..f47fdcfec1bf 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -87,7 +87,7 @@ Others See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. -- **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with heading. See :ref:`table-sync-rows-with-headings` for details. +- **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with headings. See :ref:`table-sync-rows-with-headings` for details. Message Changes *************** From 64130a27b5bd6440f2dcfc30b44410f669651f51 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 10 Apr 2023 08:58:37 +0900 Subject: [PATCH 129/485] fix: log message In error messages, we do not place a dot in the sentence when it comes `:` + word. --- system/Router/RouteCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 0ae204f140be..97f85400cdee 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -292,7 +292,7 @@ public function loadRoutes(string $routesFile = APPPATH . 'Config/Routes.php') foreach ($routeFiles as $routesFile) { if (! is_file($routesFile)) { - log_message('warning', 'Routes file not found: ' . $routesFile . '.'); + log_message('warning', sprintf('Routes file not found: "%s"', $routesFile)); continue; } From d02735ea6ca9cc66945b15c5d8773a56128afd57 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 8 Apr 2023 12:48:08 +0900 Subject: [PATCH 130/485] feat: module routing for Auto Routing Improved --- app/Config/Routing.php | 13 ++++++++++ system/Router/AutoRouterImproved.php | 10 +++++++ .../system/Router/AutoRouterImprovedTest.php | 26 +++++++++++++++++-- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/app/Config/Routing.php b/app/Config/Routing.php index 73d9a93f0f4b..e3183d2e8db8 100644 --- a/app/Config/Routing.php +++ b/app/Config/Routing.php @@ -97,4 +97,17 @@ class Routing extends BaseRouting * Default: false */ public bool $prioritize = false; + + /** + * Map of URI segments and namespaces. For Auto Routing (Improved). + * + * The key is the first URI segment. The value is the controller namespace. + * E.g., + * [ + * 'blog' => 'Acme\Blog\Controllers', + * ] + * + * @var array [ uri_segment => namespace ] + */ + public array $moduleRoutes = []; } diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 87409e78f107..0be6ed39e11c 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -13,6 +13,7 @@ use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Router\Exceptions\MethodNotFoundException; +use Config\Routing; use ReflectionClass; use ReflectionException; @@ -112,6 +113,15 @@ public function getRoute(string $uri): array { $segments = explode('/', $uri); + // Check for Module Routes. + if ($routingConfig = config(Routing::class)) { + if (array_key_exists($segments[0], $routingConfig->moduleRoutes)) { + $uriSegment = array_shift($segments); + + $this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\') . '\\'; + } + } + // WARNING: Directories get shifted out of the segments array. $nonDirSegments = $this->scanControllers($segments); diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index d6560e1eb9e6..75a5f49b8648 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Router; +use CodeIgniter\Config\Factories; use CodeIgniter\Config\Services; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Router\Controllers\Dash_folder\Dash_controller; @@ -39,11 +40,11 @@ protected function setUp(): void $this->collection = new RouteCollection(Services::locator(), $moduleConfig, new Routing()); } - private function createNewAutoRouter(string $httpVerb = 'get'): AutoRouterImproved + private function createNewAutoRouter(string $httpVerb = 'get', $namespace = 'CodeIgniter\Router\Controllers'): AutoRouterImproved { return new AutoRouterImproved( [], - 'CodeIgniter\Router\Controllers', + $namespace, $this->collection->getDefaultController(), $this->collection->getDefaultMethod(), true, @@ -66,6 +67,27 @@ public function testAutoRouteFindsDefaultControllerAndMethodGet() $this->assertSame([], $params); } + public function testAutoRouteFindsModuleDefaultControllerAndMethodGet() + { + $config = config(Routing::class); + $config->moduleRoutes = [ + 'test' => 'CodeIgniter\Router\Controllers', + ]; + Factories::injectMock('config', Routing::class, $config); + + $this->collection->setDefaultController('Index'); + + $router = $this->createNewAutoRouter('get', 'App/Controllers'); + + [$directory, $controller, $method, $params] + = $router->getRoute('test'); + + $this->assertNull($directory); + $this->assertSame('\\' . Index::class, $controller); + $this->assertSame('getIndex', $method); + $this->assertSame([], $params); + } + public function testAutoRouteFindsDefaultControllerAndMethodPost() { $this->collection->setDefaultController('Index'); From 2ea49afdfc4b405721fe805954cc4d400bd293bc Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 8 Apr 2023 13:33:01 +0900 Subject: [PATCH 131/485] feat: add Module Routing to `spark routes` --- system/Commands/Utilities/Routes.php | 17 +++++++++++++++++ .../AutoRouterImproved/AutoRouteCollector.php | 18 ++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index 3b0112db828a..b40a1cbe41bb 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -18,6 +18,7 @@ use CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\AutoRouteCollector as AutoRouteCollectorImproved; use CodeIgniter\Commands\Utilities\Routes\FilterCollector; use CodeIgniter\Commands\Utilities\Routes\SampleURIGenerator; +use Config\Routing; use Config\Services; /** @@ -152,6 +153,22 @@ public function run(array $params) ); $autoRoutes = $autoRouteCollector->get(); + + // Check for Module Routes. + if ($routingConfig = config(Routing::class)) { + foreach ($routingConfig->moduleRoutes as $uri => $namespace) { + $autoRouteCollector = new AutoRouteCollectorImproved( + $namespace, + $collection->getDefaultController(), + $collection->getDefaultMethod(), + $methods, + $collection->getRegisteredControllers('*'), + $uri + ); + + $autoRoutes = array_merge($autoRoutes, $autoRouteCollector->get()); + } + } } else { $autoRouteCollector = new AutoRouteCollector( $collection->getDefaultNamespace(), diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php index f2de8e4b4a6d..f3b1c8169d3c 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php @@ -35,6 +35,11 @@ final class AutoRouteCollector */ private array $protectedControllers; + /** + * @var string URI prefix for Module Routing + */ + private string $prefix; + /** * @param string $namespace namespace to search */ @@ -43,13 +48,15 @@ public function __construct( string $defaultController, string $defaultMethod, array $httpMethods, - array $protectedControllers + array $protectedControllers, + string $prefix = '' ) { $this->namespace = $namespace; $this->defaultController = $defaultController; $this->defaultMethod = $defaultMethod; $this->httpMethods = $httpMethods; $this->protectedControllers = $protectedControllers; + $this->prefix = $prefix; } /** @@ -82,9 +89,16 @@ public function get(): array $routes = $this->addFilters($routes); foreach ($routes as $item) { + $route = $item['route'] . $item['route_params']; + if ($this->prefix !== '' && $route === '/') { + $route = $this->prefix; + } elseif ($this->prefix !== '') { + $route = $this->prefix . '/' . $route; + } + $tbody[] = [ strtoupper($item['method']) . '(auto)', - $item['route'] . $item['route_params'], + $route, '', $item['handler'], $item['before'], From 7f817fd435389cfd391ca50d91f5c20d232c9ae1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 8 Apr 2023 14:00:06 +0900 Subject: [PATCH 132/485] refactor: remove if --- system/Router/AutoRouterImproved.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 0be6ed39e11c..000afda126bd 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -114,12 +114,12 @@ public function getRoute(string $uri): array $segments = explode('/', $uri); // Check for Module Routes. - if ($routingConfig = config(Routing::class)) { - if (array_key_exists($segments[0], $routingConfig->moduleRoutes)) { - $uriSegment = array_shift($segments); - - $this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\') . '\\'; - } + if ( + ($routingConfig = config(Routing::class)) + && array_key_exists($segments[0], $routingConfig->moduleRoutes) + ) { + $uriSegment = array_shift($segments); + $this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\') . '\\'; } // WARNING: Directories get shifted out of the segments array. From e1a1f89be813b105eaf31b34129af8b5db05bd3d Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 8 Apr 2023 14:07:07 +0900 Subject: [PATCH 133/485] refactor: by rector --- system/Commands/Utilities/Routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index b40a1cbe41bb..83ca40a7c1bd 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -166,7 +166,7 @@ public function run(array $params) $uri ); - $autoRoutes = array_merge($autoRoutes, $autoRouteCollector->get()); + $autoRoutes = [...$autoRoutes, ...$autoRouteCollector->get()]; } } } else { From 9287cb0ca7123fea7f698e746622b97f6d666405 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 10 Apr 2023 10:35:04 +0900 Subject: [PATCH 134/485] docs: add docs --- user_guide_src/source/changelogs/v4.4.0.rst | 2 ++ user_guide_src/source/incoming/routing.rst | 26 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 3c4fe991d3c6..29a8b030f9ba 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -88,6 +88,8 @@ Others the ``Content-Disposition: inline`` header to display the file in the browser. See :ref:`open-file-in-browser` for details. - **View:** Added optional 2nd parameter ``$saveData`` on ``renderSection()`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. +- **Auto Routing (Improved)**: Now you can route to Modules. + See :ref:`auto-routing-improved-module-routing` for details. - **Auto Routing (Improved)**: Now you can use URI without a method name like ``product/15`` where ``15`` is an arbitrary number. See :ref:`controller-default-method-fallback` for details. diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 066009beda65..b2bbde1aea66 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -716,6 +716,32 @@ In this example, if the user were to visit **example.com/products**, and a ``Pro .. note:: You cannot access the controller with the URI of the default method name. In the example above, you can access **example.com/products**, but if you access **example.com/products/listall**, it will be not found. +.. _auto-routing-improved-module-routing: + +Module Routing +============== + +.. versionadded:: 4.4.0 + +You can use auto routing even if you use :doc:`../general/modules` and place +the controllers in a different namespace. + +To route to a module, the ``$moduleRoutes`` property in **app/Config/Routing.php** +must be set:: + + public array $moduleRoutes = [ + 'blog' => 'Acme\Blog\Controllers', + ]; + +The key is the first URI segment for the module, and the value is the controller +namespace. In the above configuration, **http://localhost:8080/blog/foo/bar** +will be routed to ``Acme\Blog\Controllers\Foo::getBar()``. + +.. note:: If you define ``$moduleRoutes``, the routing for the module takes + precedence. In the above example, even if you have the ``App\Controllers\Blog`` + controller, **http://localhost:8080/blog** will be routed to the default + controller ``Acme\Blog\Controllers\Home``. + .. _auto-routing-legacy: Auto Routing (Legacy) From a72ea47e67714b390e739c39e09f51996e990972 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 10 Apr 2023 11:08:30 +0900 Subject: [PATCH 135/485] fix: `spark routes` filters output --- .../AutoRouterImproved/AutoRouteCollector.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php index f3b1c8169d3c..2b6096016f7e 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php @@ -90,6 +90,8 @@ public function get(): array foreach ($routes as $item) { $route = $item['route'] . $item['route_params']; + + // For module routing if ($this->prefix !== '' && $route === '/') { $route = $this->prefix; } elseif ($this->prefix !== '') { @@ -115,13 +117,22 @@ private function addFilters($routes) $filterCollector = new FilterCollector(true); foreach ($routes as &$route) { + $routePath = $route['route']; + + // For module routing + if ($this->prefix !== '' && $route === '/') { + $routePath = $this->prefix; + } elseif ($this->prefix !== '') { + $routePath = $this->prefix . '/' . $routePath; + } + // Search filters for the URI with all params $sampleUri = $this->generateSampleUri($route); - $filtersLongest = $filterCollector->get($route['method'], $route['route'] . $sampleUri); + $filtersLongest = $filterCollector->get($route['method'], $routePath . $sampleUri); // Search filters for the URI without optional params $sampleUri = $this->generateSampleUri($route, false); - $filtersShortest = $filterCollector->get($route['method'], $route['route'] . $sampleUri); + $filtersShortest = $filterCollector->get($route['method'], $routePath . $sampleUri); // Get common array elements $filters['before'] = array_intersect($filtersLongest['before'], $filtersShortest['before']); From 641477b5fa9abe1c9b005abcb86062e9192bb604 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 10 Apr 2023 14:28:12 +0900 Subject: [PATCH 136/485] test: fix incorrect test --- tests/system/Validation/ValidationTest.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 2c6d52337fe9..36956fc3f771 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -713,19 +713,17 @@ public function testInvalidRule(): void public function testRawInput(): void { - $rawstring = 'username=admin001&role=administrator&usepass=0'; - - $data = [ - 'username' => 'admin001', - 'role' => 'administrator', - 'usepass' => 0, - ]; - + $rawstring = 'username=admin001&role=administrator&usepass=0'; $config = new App(); $config->baseURL = 'http://example.com/'; + $request = new IncomingRequest($config, new URI(), $rawstring, new UserAgent()); + + $rules = [ + 'role' => 'required|min_length[5]', + ]; + $validated = $this->validation->withRequest($request->withMethod('patch'))->setRules($rules)->run(); - $request = new IncomingRequest($config, new URI(), $rawstring, new UserAgent()); - $this->validation->withRequest($request->withMethod('patch'))->run($data); + $this->assertTrue($validated); $this->assertSame([], $this->validation->getErrors()); } From c418c84459549ec057e94fbb27072ecedb88ce1d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 09:36:30 +0900 Subject: [PATCH 137/485] test: rename variable name --- tests/system/Validation/ValidationTest.php | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 36956fc3f771..245f4c8a3546 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -259,9 +259,9 @@ public function testClosureRule(): void ); $data = ['foo' => 'xyz']; - $return = $this->validation->run($data); + $result = $this->validation->run($data); - $this->assertFalse($return); + $this->assertFalse($result); $this->assertSame( ['foo' => 'The value is not "abc"'], $this->validation->getErrors() @@ -286,9 +286,9 @@ static function ($value, $data, &$error, $field) { ]); $data = ['foo' => 'xyz']; - $return = $this->validation->run($data); + $result = $this->validation->run($data); - $this->assertFalse($return); + $this->assertFalse($result); $this->assertSame( ['foo' => 'The foo value is not "abc"'], $this->validation->getErrors() @@ -309,9 +309,9 @@ public function testClosureRuleWithLabel(): void ]); $data = ['secret' => 'xyz']; - $return = $this->validation->run($data); + $result = $this->validation->run($data); - $this->assertFalse($return); + $this->assertFalse($result); $this->assertSame( ['secret' => 'The シークレット is invalid'], $this->validation->getErrors() @@ -721,9 +721,9 @@ public function testRawInput(): void $rules = [ 'role' => 'required|min_length[5]', ]; - $validated = $this->validation->withRequest($request->withMethod('patch'))->setRules($rules)->run(); + $result = $this->validation->withRequest($request->withMethod('patch'))->setRules($rules)->run(); - $this->assertTrue($validated); + $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); } @@ -746,12 +746,12 @@ public function testJsonInput(): void $rules = [ 'role' => 'required|min_length[5]', ]; - $validated = $this->validation + $result = $this->validation ->withRequest($request->withMethod('patch')) ->setRules($rules) ->run(); - $this->assertTrue($validated); + $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); unset($_SERVER['CONTENT_TYPE']); @@ -782,12 +782,12 @@ public function testJsonInputObjectArray(): void $rules = [ 'p' => 'required|array_count[2]', ]; - $validated = $this->validation + $result = $this->validation ->withRequest($request->withMethod('patch')) ->setRules($rules) ->run(); - $this->assertFalse($validated); + $this->assertFalse($result); $this->assertSame(['p' => 'Validation.array_count'], $this->validation->getErrors()); unset($_SERVER['CONTENT_TYPE']); From 2992fefdbf032b41aeeac5592d1c8fe8fe1237f3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 09:47:24 +0900 Subject: [PATCH 138/485] test: add assertions for run() results --- tests/system/Validation/ValidationTest.php | 44 +++++++++++++++------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 245f4c8a3546..99dc287d39ca 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -437,8 +437,9 @@ public function testRunWithCustomErrors(): void ], ]; $this->validation->setRules(['foo' => 'is_numeric', 'bar' => 'is_numeric'], $messages); - $this->validation->run($data); + $result = $this->validation->run($data); + $this->assertFalse($result); $this->assertSame('Nope. Not a number.', $this->validation->getError('foo')); $this->assertSame('No. Not a number.', $this->validation->getError('bar')); } @@ -464,8 +465,9 @@ public function testSetRuleWithCustomErrors(): void ['bar' => 'is_numeric'], ['is_numeric' => 'Nope. Not a number.'] ); - $this->validation->run($data); + $result = $this->validation->run($data); + $this->assertFalse($result); $this->assertSame('Nope. Not a number.', $this->validation->getError('foo')); $this->assertSame('Nope. Not a number.', $this->validation->getError('bar')); } @@ -493,7 +495,9 @@ public function testGetErrors(): void { $data = ['foo' => 'notanumber']; $this->validation->setRules(['foo' => 'is_numeric']); - $this->validation->run($data); + $result = $this->validation->run($data); + + $this->assertFalse($result); $this->assertSame(['foo' => 'Validation.is_numeric'], $this->validation->getErrors()); } @@ -501,7 +505,9 @@ public function testGetErrorsWhenNone(): void { $data = ['foo' => 123]; $this->validation->setRules(['foo' => 'is_numeric']); - $this->validation->run($data); + $result = $this->validation->run($data); + + $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); } @@ -515,7 +521,7 @@ public function testSetErrors(): void public function testRulesReturnErrors(): void { $this->validation->setRules(['foo' => 'customError']); - $this->validation->run(['foo' => 'bar']); + $this->assertFalse($this->validation->run(['foo' => 'bar'])); $this->assertSame(['foo' => 'My lovely error'], $this->validation->getErrors()); } @@ -564,6 +570,7 @@ public function testSetRuleGroupWithCustomErrorMessage(): void $this->validation->reset(); $this->validation->setRuleGroup('login'); $this->validation->run(['username' => 'codeigniter']); + $this->assertSame([ 'password' => 'custom password required error msg.', ], $this->validation->getErrors()); @@ -915,7 +922,10 @@ public function testTagReplacement(): void 'min_length' => 'Supplied value ({value}) for {field} must have at least {param} characters.', ]] ); - $this->validation->run($data); + $result = $this->validation->run($data); + + $this->assertFalse($result); + $errors = $this->validation->getErrors(); if (! isset($errors['Username'])) { @@ -932,8 +942,10 @@ public function testRulesForObjectField(): void 'configuration' => 'required|check_object_rule', ]); - $data = (object) ['configuration' => (object) ['first' => 1, 'second' => 2]]; - $this->validation->run((array) $data); + $data = (object) ['configuration' => (object) ['first' => 1, 'second' => 2]]; + $result = $this->validation->run((array) $data); + + $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); $this->validation->reset(); @@ -941,9 +953,10 @@ public function testRulesForObjectField(): void 'configuration' => 'required|check_object_rule', ]); - $data = (object) ['configuration' => (object) ['first1' => 1, 'second' => 2]]; - $this->validation->run((array) $data); + $data = (object) ['configuration' => (object) ['first1' => 1, 'second' => 2]]; + $result = $this->validation->run((array) $data); + $this->assertFalse($result); $this->assertSame([ 'configuration' => 'Validation.check_object_rule', ], $this->validation->getErrors()); @@ -1166,7 +1179,6 @@ public function testTranslatedLabelWithCustomErrorMessage(): void public function testTranslatedLabelTagReplacement(): void { $data = ['Username' => 'Pizza']; - $this->validation->setRules( ['Username' => [ 'label' => 'Foo.bar', @@ -1176,8 +1188,10 @@ public function testTranslatedLabelTagReplacement(): void 'min_length' => 'Foo.bar.min_length2', ]] ); + $result = $this->validation->run($data); + + $this->assertFalse($result); - $this->validation->run($data); $errors = $this->validation->getErrors(); if (! isset($errors['Username'])) { @@ -1505,8 +1519,9 @@ public function testNestedArrayThrowsException(): void 'debit_amount' => '1500', 'beneficiaries_accounts' => [], ]; - $this->validation->run($data); + $result = $this->validation->run($data); + $this->assertFalse($result); $this->assertSame([ 'beneficiaries_accounts.*.account_number' => 'The BENEFICIARY ACCOUNT NUMBER field must be exactly 5 characters in length.', 'beneficiaries_accounts.*.credit_amount' => 'The CREDIT AMOUNT field is required.', @@ -1536,8 +1551,9 @@ public function testNestedArrayThrowsException(): void ], ], ]; - $this->validation->run($data); + $result = $this->validation->run($data); + $this->assertFalse($result); $this->assertSame([ 'beneficiaries_accounts.account_3.account_number' => 'The BENEFICIARY ACCOUNT NUMBER field must be exactly 5 characters in length.', 'beneficiaries_accounts.account_2.credit_amount' => 'The CREDIT AMOUNT field is required.', From f8a7813029e6027135660bbf88de466ed5c2eb73 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 11:19:31 +0900 Subject: [PATCH 139/485] feat: add DotArrayFilter for Validation::getValidated() --- system/Validation/DotArrayFilter.php | 109 +++++++++++ .../system/Validation/DotArrayFilterTest.php | 183 ++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 system/Validation/DotArrayFilter.php create mode 100644 tests/system/Validation/DotArrayFilterTest.php diff --git a/system/Validation/DotArrayFilter.php b/system/Validation/DotArrayFilter.php new file mode 100644 index 000000000000..cead4f6bb414 --- /dev/null +++ b/system/Validation/DotArrayFilter.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Validation; + +final class DotArrayFilter +{ + /** + * Creates a new array with only the elements specified in dot array syntax. + * + * This code comes from the dot_array_search() function. + * + * @param array $indexes The dot array syntax pattern to use for filtering. + * @param array $array The array to filter. + * + * @return array The filtered array. + */ + public static function run(array $indexes, array $array): array + { + $result = []; + + foreach ($indexes as $index) { + // See https://regex101.com/r/44Ipql/1 + $segments = preg_split( + '/(? str_replace('\.', '.', $key), + $segments + ); + + $result = array_merge_recursive($result, self::filter($segments, $array)); + } + + return $result; + } + + /** + * Used by `run()` to recursively filter the array with wildcards. + * + * @param array $indexes The dot array syntax pattern to use for filtering. + * @param array $array The array to filter. + * + * @return array The filtered array. + */ + private static function filter(array $indexes, array $array): array + { + // If index is empty, returns empty array. + if ($indexes === []) { + return []; + } + + // Grab the current index. + $currentIndex = array_shift($indexes); + + if (! isset($array[$currentIndex]) && $currentIndex !== '*') { + return []; + } + + // Handle Wildcard (*) + if ($currentIndex === '*') { + $answer = []; + + foreach ($array as $key => $value) { + if (! is_array($value)) { + continue; + } + + $result = self::filter($indexes, $value); + + if ($result !== []) { + $answer[$key] = $result; + } + } + + return $answer; + } + + // If this is the last index, make sure to return it now, + // and not try to recurse through things. + if (empty($indexes)) { + return [$currentIndex => $array[$currentIndex]]; + } + + // Do we need to recursively filter this value? + if (is_array($array[$currentIndex]) && $array[$currentIndex] !== []) { + $result = self::filter($indexes, $array[$currentIndex]); + + if ($result !== []) { + return [$currentIndex => $result]; + } + } + + // Otherwise, not found. + return []; + } +} diff --git a/tests/system/Validation/DotArrayFilterTest.php b/tests/system/Validation/DotArrayFilterTest.php new file mode 100644 index 000000000000..6d3b380f517a --- /dev/null +++ b/tests/system/Validation/DotArrayFilterTest.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Validation; + +use CodeIgniter\Test\CIUnitTestCase; + +/** + * @internal + * + * @group Others + */ +final class DotArrayFilterTest extends CIUnitTestCase +{ + public function testRunReturnEmptyArray() + { + $data = []; + + $result = DotArrayFilter::run(['foo.bar'], $data); + + $this->assertSame([], $result); + } + + public function testRunReturnEmptyArrayMissingValue() + { + $data = [ + 'foo' => [ + 'bar' => 23, + ], + ]; + + $result = DotArrayFilter::run(['foo.baz'], $data); + + $this->assertSame([], $result); + } + + public function testRunReturnEmptyArrayEmptyIndex() + { + $data = [ + 'foo' => [ + 'bar' => 23, + ], + ]; + + $result = DotArrayFilter::run([''], $data); + + $this->assertSame([], $result); + } + + public function testRunEarlyIndex() + { + $data = [ + 'foo' => [ + 'bar' => 23, + ], + ]; + + $result = DotArrayFilter::run(['foo'], $data); + + $this->assertSame($data, $result); + } + + public function testRunWildcard() + { + $data = [ + 'foo' => [ + 'bar' => [ + 'baz' => 23, + ], + ], + ]; + + $result = DotArrayFilter::run(['foo.*.baz'], $data); + + $this->assertSame($data, $result); + } + + public function testRunWildcardWithMultipleChoices() + { + $data = [ + 'foo' => [ + 'buzz' => [ + 'fizz' => 11, + ], + 'bar' => [ + 'baz' => 23, + ], + ], + ]; + + $result = DotArrayFilter::run(['foo.*.fizz', 'foo.*.baz'], $data); + + $this->assertSame($data, $result); + } + + public function testRunNestedNotFound() + { + $data = [ + 'foo' => [ + 'buzz' => [ + 'fizz' => 11, + ], + 'bar' => [ + 'baz' => 23, + ], + ], + ]; + + $result = DotArrayFilter::run(['foo.*.notthere'], $data); + + $this->assertSame([], $result); + } + + public function testRunIgnoresLastWildcard() + { + $data = [ + 'foo' => [ + 'bar' => [ + 'baz' => 23, + ], + ], + ]; + + $result = DotArrayFilter::run(['foo.bar.*'], $data); + + $this->assertSame($data, $result); + } + + public function testRunNestedArray() + { + $array = [ + 'user' => [ + 'name' => 'John', + 'age' => 30, + 'email' => 'john@example.com', + 'preferences' => [ + 'theme' => 'dark', + 'language' => 'en', + 'notifications' => [ + 'email' => true, + 'push' => false, + ], + ], + ], + 'product' => [ + 'name' => 'Acme Product', + 'description' => 'This is a great product!', + 'price' => 19.99, + ], + ]; + + $result = DotArrayFilter::run([ + 'user.name', + 'user.preferences.language', + 'user.preferences.notifications.email', + 'product.name', + ], $array); + + $expected = [ + 'user' => [ + 'name' => 'John', + 'preferences' => [ + 'language' => 'en', + 'notifications' => [ + 'email' => true, + ], + ], + ], + 'product' => [ + 'name' => 'Acme Product', + ], + ]; + $this->assertSame($expected, $result); + } +} From a8412edee905ddd4074768ef63f9dff7ac4128d5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 10 Apr 2023 14:29:50 +0900 Subject: [PATCH 140/485] feat: add Validation::getValidated() for getting validated data --- system/Validation/Validation.php | 35 ++++++++++++++++++++-- tests/system/Validation/ValidationTest.php | 17 +++++++---- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index c22f38ae2413..b4131af95305 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -55,6 +55,13 @@ class Validation implements ValidationInterface */ protected $data = []; + /** + * The data that was actually validated. + * + * @var array + */ + protected $validated = []; + /** * Any generated errors during validation. * 'key' is the alias, 'value' is the message. @@ -109,7 +116,12 @@ public function __construct($config, RendererInterface $view) */ public function run(?array $data = null, ?string $group = null, ?string $dbGroup = null): bool { - $data ??= $this->data; + if ($data === null) { + $data = $this->data; + } else { + // Store data to validate. + $this->data = $data; + } // i.e. is_unique $data['DBGroup'] = $dbGroup; @@ -171,7 +183,17 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup } } - return $this->getErrors() === []; + if ($this->getErrors() === []) { + // Store data that was actually validated. + $this->validated = DotArrayFilter::run( + array_keys($this->rules), + $this->data + ); + + return true; + } + + return false; } /** @@ -188,6 +210,14 @@ public function check($value, string $rule, array $errors = []): bool return $this->setRule('check', null, $rule, $errors)->run(['check' => $value]); } + /** + * Returns actually validated data. + */ + public function getValidated(): array + { + return $this->validated; + } + /** * Runs all of $rules against $field, until one fails, or * all of them have been processed. If one fails, it adds @@ -827,6 +857,7 @@ protected function splitRules(string $rules): array public function reset(): ValidationInterface { $this->data = []; + $this->validated = []; $this->rules = []; $this->errors = []; $this->customErrors = []; diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 99dc287d39ca..1787377348e1 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -234,13 +234,16 @@ public function testRunReturnsFalseWithNothingToDo() { $this->validation->setRules([]); $this->assertFalse($this->validation->run([])); + $this->assertSame([], $this->validation->getValidated()); } public function testRunDoesTheBasics(): void { $data = ['foo' => 'notanumber']; $this->validation->setRules(['foo' => 'is_numeric']); + $this->assertFalse($this->validation->run($data)); + $this->assertSame([], $this->validation->getValidated()); } public function testClosureRule(): void @@ -266,6 +269,7 @@ public function testClosureRule(): void ['foo' => 'The value is not "abc"'], $this->validation->getErrors() ); + $this->assertSame([], $this->validation->getValidated()); } public function testClosureRuleWithParamError(): void @@ -293,6 +297,7 @@ static function ($value, $data, &$error, $field) { ['foo' => 'The foo value is not "abc"'], $this->validation->getErrors() ); + $this->assertSame([], $this->validation->getValidated()); } public function testClosureRuleWithLabel(): void @@ -732,23 +737,22 @@ public function testRawInput(): void $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); + $this->assertSame(['role' => 'administrator'], $this->validation->getValidated()); } public function testJsonInput(): void { + $_SERVER['CONTENT_TYPE'] = 'application/json'; + $data = [ 'username' => 'admin001', 'role' => 'administrator', 'usepass' => 0, ]; - $json = json_encode($data); - - $_SERVER['CONTENT_TYPE'] = 'application/json'; - + $json = json_encode($data); $config = new App(); $config->baseURL = 'http://example.com/'; - - $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); + $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); $rules = [ 'role' => 'required|min_length[5]', @@ -760,6 +764,7 @@ public function testJsonInput(): void $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); + $this->assertSame(['role' => 'administrator'], $this->validation->getValidated()); unset($_SERVER['CONTENT_TYPE']); } From 83a5d5c5e32e5b058890f51bdd7abe6e9f757edf Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 13:36:58 +0900 Subject: [PATCH 141/485] docs: add docs --- user_guide_src/source/changelogs/v4.4.0.rst | 3 +++ .../source/libraries/validation.rst | 13 ++++++++++++ .../source/libraries/validation/043.php | 21 +++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 user_guide_src/source/libraries/validation/043.php diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 3c4fe991d3c6..dd2fc83ef8b7 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -78,6 +78,9 @@ Model Libraries ========= +- **Validation:** Added ``Validation::getValidated()`` method that gets + the actual validated data. See :ref:`validation-getting-validated-data` for details. + Helpers and Functions ===================== diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 823a8b899bb3..0eebba50b168 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -345,6 +345,19 @@ Validate one value against a rule: .. literalinclude:: validation/012.php +.. _validation-getting-validated-data: + +Getting Validated Data +====================== + +.. versionadded:: 4.4.0 + +The actual validated data can be retrieved with the ``getValidated()`` method. +This method returns an array of only those elements that have been validated by +the validation rules. + +.. literalinclude:: validation/043.php + Saving Sets of Validation Rules to the Config File ================================================== diff --git a/user_guide_src/source/libraries/validation/043.php b/user_guide_src/source/libraries/validation/043.php new file mode 100644 index 000000000000..7f3a0e0e0902 --- /dev/null +++ b/user_guide_src/source/libraries/validation/043.php @@ -0,0 +1,21 @@ +setRules([ + 'username' => 'required', + 'password' => 'required|min_length[10]', +]); + +$data = [ + 'username' => 'john', + 'password' => 'BPi-$Swu7U5lm$dX', + 'csrf_token' => '8b9218a55906f9dcc1dc263dce7f005a', +]; + +if ($validation->run($data)) { + $validatedData = $validation->getValidated(); + // $validatedData = [ + // 'username' => 'john', + // 'password' => 'BPi-$Swu7U5lm$dX', + // ]; +} From e324b0741d17dd79f9e1ffad22ba2f4473917f12 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 20:27:42 +0900 Subject: [PATCH 142/485] docs: update sample code for $this->validate() and explanations --- .../source/incoming/controllers.rst | 21 ++++++++++++------- .../source/incoming/controllers/004.php | 8 ++++++- .../source/incoming/controllers/005.php | 8 ++++++- .../source/libraries/validation.rst | 15 +++++++++++-- .../source/libraries/validation/001.php | 5 +++-- .../source/libraries/validation/008.php | 10 ++++++++- .../source/libraries/validation/045.php | 18 ++++++++++++++++ 7 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 user_guide_src/source/libraries/validation/045.php diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index 7a0233482527..18388e76b6a5 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -98,19 +98,24 @@ and in the optional second parameter, an array of custom error messages to displ if the items are not valid. Internally, this uses the controller's ``$this->request`` instance to get the data to be validated. -.. warning:: - The ``validate()`` method uses :ref:`Validation::withRequest() ` method. - It validates data from :ref:`$request->getJSON() ` - or :ref:`$request->getRawInput() ` - or :ref:`$request->getVar() `. - Which data is used depends on the request. Remember that an attacker is free to send any request to - the server. - The :doc:`Validation Library docs ` have details on rule and message array formats, as well as available rules: .. literalinclude:: controllers/004.php +.. warning:: When you use the ``validate()`` method, you should use the + :ref:`getValidated() ` method to get the + validated data. Because the ``validate()`` method uses the + :ref:`Validation::withRequest() ` method internally, + and it validates data from + :ref:`$request->getJSON() ` + or :ref:`$request->getRawInput() ` + or :ref:`$request->getVar() `, and an attacker + could change what data is validated. + +.. note:: The :ref:`$this->validator->getValidated() ` + method can be used since v4.4.0. + If you find it simpler to keep the rules in the configuration file, you can replace the ``$rules`` array with the name of the group as defined in **app/Config/Validation.php**: diff --git a/user_guide_src/source/incoming/controllers/004.php b/user_guide_src/source/incoming/controllers/004.php index 9216bab50032..afdf0a145852 100644 --- a/user_guide_src/source/incoming/controllers/004.php +++ b/user_guide_src/source/incoming/controllers/004.php @@ -10,11 +10,17 @@ public function updateUser(int $userID) 'email' => "required|is_unique[users.email,id,{$userID}]", 'name' => 'required|alpha_numeric_spaces', ])) { + // The validation failed. return view('users/update', [ 'errors' => $this->validator->getErrors(), ]); } - // do something here if successful... + // The validation was successful. + + // Get the validated data. + $validData = $this->validator->getValidated(); + + // ... } } diff --git a/user_guide_src/source/incoming/controllers/005.php b/user_guide_src/source/incoming/controllers/005.php index 48e936b60595..34170437355d 100644 --- a/user_guide_src/source/incoming/controllers/005.php +++ b/user_guide_src/source/incoming/controllers/005.php @@ -7,11 +7,17 @@ class UserController extends BaseController public function updateUser(int $userID) { if (! $this->validate('userRules')) { + // The validation failed. return view('users/update', [ 'errors' => $this->validator->getErrors(), ]); } - // do something here if successful... + // The validation was successful. + + // Get the validated data. + $validData = $this->validator->getValidated(); + + // ... } } diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 2c9b5cb6994f..83402a96036e 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -128,6 +128,9 @@ this code and save it to your **app/Controllers/** folder: In previous versions, you need to use ``if (strtolower($this->request->getMethod()) !== 'post')``. +.. note:: The :ref:`$this->validator->getValidated() ` + method can be used since v4.4.0. + The Routes ========== @@ -331,14 +334,20 @@ data to be validated: .. literalinclude:: validation/008.php -.. note:: This method gets JSON data from +.. warning:: When you use this method, you should use the + :ref:`getValidated() ` method to get the + validated data. Because this method gets JSON data from :ref:`$request->getJSON() ` when the request is a JSON request (``Content-Type: application/json``), or gets Raw data from :ref:`$request->getRawInput() ` when the request is a PUT, PATCH, DELETE request and is not HTML form post (``Content-Type: multipart/form-data``), - or gets data from :ref:`$request->getVar() `. + or gets data from :ref:`$request->getVar() `, + and an attacker could change what data is validated. + +.. note:: The :ref:`getValidated() ` + method can be used since v4.4.0. *********************** Working with Validation @@ -393,6 +402,8 @@ the validation rules. .. literalinclude:: validation/043.php +.. literalinclude:: validation/045.php + Saving Sets of Validation Rules to the Config File ================================================== diff --git a/user_guide_src/source/libraries/validation/001.php b/user_guide_src/source/libraries/validation/001.php index af2acb754a7b..e797b21a6ae0 100644 --- a/user_guide_src/source/libraries/validation/001.php +++ b/user_guide_src/source/libraries/validation/001.php @@ -2,8 +2,6 @@ namespace App\Controllers; -use Config\Services; - class Form extends BaseController { protected $helpers = ['form']; @@ -20,6 +18,9 @@ public function index() return view('signup'); } + // If you want to get the validated data. + $validData = $this->validator->getValidated(); + return view('success'); } } diff --git a/user_guide_src/source/libraries/validation/008.php b/user_guide_src/source/libraries/validation/008.php index 1b570f1aad82..38656dda37cb 100644 --- a/user_guide_src/source/libraries/validation/008.php +++ b/user_guide_src/source/libraries/validation/008.php @@ -1,3 +1,11 @@ withRequest($this->request)->run(); +$validation = \Config\Services::validation(); +$request = \Config\Services::request(); + +if ($validation->withRequest($request)->run()) { + // If you want to get the validated data. + $validData = $validation->getValidated(); + + // ... +} diff --git a/user_guide_src/source/libraries/validation/045.php b/user_guide_src/source/libraries/validation/045.php new file mode 100644 index 000000000000..1becb578dc21 --- /dev/null +++ b/user_guide_src/source/libraries/validation/045.php @@ -0,0 +1,18 @@ +validate([ + 'username' => 'required', + 'password' => 'required|min_length[10]', +])) { + // The validation failed. + return view('login', [ + 'errors' => $this->validator->getErrors(), + ]); +} + +// The validation was successful. + +// Get the validated data. +$validData = $this->validator->getValidated(); From 29e8dd21cc6e1dd6e0ecbb163e489c6f2698bf10 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 08:52:38 +0900 Subject: [PATCH 143/485] docs: swap sample files 043.php is sample for Validation::run() in develop. The contents of that file should not change in 4.4 branch. The change was due to a mistake made during the merge. --- .../source/libraries/validation.rst | 4 ++-- .../source/libraries/validation/043.php | 24 +++++-------------- .../source/libraries/validation/044.php | 24 ++++++++++++++----- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 83402a96036e..43b597cee776 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -366,7 +366,7 @@ The optional third parameter ``$dbGroup`` is the database group to use. This method returns true if the validation is successful. -.. literalinclude:: validation/044.php +.. literalinclude:: validation/043.php Running Multiple Validations ============================ @@ -400,7 +400,7 @@ The actual validated data can be retrieved with the ``getValidated()`` method. This method returns an array of only those elements that have been validated by the validation rules. -.. literalinclude:: validation/043.php +.. literalinclude:: validation/044.php .. literalinclude:: validation/045.php diff --git a/user_guide_src/source/libraries/validation/043.php b/user_guide_src/source/libraries/validation/043.php index 7f3a0e0e0902..5700734ce975 100644 --- a/user_guide_src/source/libraries/validation/043.php +++ b/user_guide_src/source/libraries/validation/043.php @@ -1,21 +1,9 @@ setRules([ - 'username' => 'required', - 'password' => 'required|min_length[10]', -]); - -$data = [ - 'username' => 'john', - 'password' => 'BPi-$Swu7U5lm$dX', - 'csrf_token' => '8b9218a55906f9dcc1dc263dce7f005a', -]; - -if ($validation->run($data)) { - $validatedData = $validation->getValidated(); - // $validatedData = [ - // 'username' => 'john', - // 'password' => 'BPi-$Swu7U5lm$dX', - // ]; +if (! $validation->run($data)) { + // handle validation errors +} +// or +if (! $validation->run($data, 'signup')) { + // handle validation errors } diff --git a/user_guide_src/source/libraries/validation/044.php b/user_guide_src/source/libraries/validation/044.php index 5700734ce975..7f3a0e0e0902 100644 --- a/user_guide_src/source/libraries/validation/044.php +++ b/user_guide_src/source/libraries/validation/044.php @@ -1,9 +1,21 @@ run($data)) { - // handle validation errors -} -// or -if (! $validation->run($data, 'signup')) { - // handle validation errors +$validation = \Config\Services::validation(); +$validation->setRules([ + 'username' => 'required', + 'password' => 'required|min_length[10]', +]); + +$data = [ + 'username' => 'john', + 'password' => 'BPi-$Swu7U5lm$dX', + 'csrf_token' => '8b9218a55906f9dcc1dc263dce7f005a', +]; + +if ($validation->run($data)) { + $validatedData = $validation->getValidated(); + // $validatedData = [ + // 'username' => 'john', + // 'password' => 'BPi-$Swu7U5lm$dX', + // ]; } From 5d535b7607f7472ae541267694684dffa85a59ec Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 09:23:07 +0900 Subject: [PATCH 144/485] docs: add upgrade_440 --- .../source/installation/upgrade_440.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 9d7d002d6efc..e282fd640fda 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -12,6 +12,25 @@ Please refer to the upgrade instructions corresponding to your installation meth :local: :depth: 2 +SECURITY +******** + +When Using $this->validate() +============================ + +There was a known potential vulnerability in :ref:`$this->validate() ` in the Controller to bypass validation. +The attack could allow developers to misinterpret unvalidated empty data as +validated and proceed with processing. + +The :ref:`Validation::getValidated() ` +method has been added to ensure that validated data is obtained. + +Therefore, when you use ``$this->validate()`` in your Controllers, you should +use the new ``Validation::getValidated()`` method to get the validated data. + +.. literalinclude:: ../libraries/validation/045.php + :lines: 2- + Breaking Changes **************** From 04a544528dcef25558556022fcc1bc47b510d43c Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 09:33:11 +0900 Subject: [PATCH 145/485] docs: remove unneeded `` method to get the @@ -367,6 +375,7 @@ The optional third parameter ``$dbGroup`` is the database group to use. This method returns true if the validation is successful. .. literalinclude:: validation/043.php + :lines: 2- Running Multiple Validations ============================ @@ -381,6 +390,7 @@ errors from previous run. Be aware that ``reset()`` will invalidate any data, ru you previously set, so ``setRules()``, ``setRuleGroup()`` etc. need to be repeated: .. literalinclude:: validation/019.php + :lines: 2- Validating 1 Value ================== @@ -388,6 +398,7 @@ Validating 1 Value Validate one value against a rule: .. literalinclude:: validation/012.php + :lines: 2- .. _validation-getting-validated-data: @@ -401,8 +412,10 @@ This method returns an array of only those elements that have been validated by the validation rules. .. literalinclude:: validation/044.php + :lines: 2- .. literalinclude:: validation/045.php + :lines: 2- Saving Sets of Validation Rules to the Config File ================================================== @@ -429,6 +442,7 @@ How to Specify Rule Group You can specify the group to use when you call the ``run()`` method: .. literalinclude:: validation/014.php + :lines: 2- How to Save Error Messages -------------------------- @@ -453,12 +467,14 @@ Getting & Setting Rule Groups This method gets a rule group from the validation configuration: .. literalinclude:: validation/017.php + :lines: 2- **Set Rule Group** This method sets a rule group from the validation configuration to the validation service: .. literalinclude:: validation/018.php + :lines: 2- Validation Placeholders ======================= @@ -469,15 +485,18 @@ the name of the field (or array key) that was passed in as ``$data`` surrounded replaced by the **value** of the matched incoming field. An example should clarify this: .. literalinclude:: validation/020.php + :lines: 2- In this set of rules, it states that the email address should be unique in the database, except for the row that has an id matching the placeholder's value. Assuming that the form POST data had the following: .. literalinclude:: validation/021.php + :lines: 2- then the ``{id}`` placeholder would be replaced with the number **4**, giving this revised rule: .. literalinclude:: validation/022.php + :lines: 2- So it will ignore the row in the database that has ``id=4`` when it verifies the email is unique. @@ -509,10 +528,12 @@ These are two ways to provide custom error messages. As the last parameter: .. literalinclude:: validation/023.php + :lines: 2- Or as a labeled style: .. literalinclude:: validation/024.php + :lines: 2- If you'd like to include a field's "human" name, or the optional parameter some rules allow for (such as max_length), or the value that was validated you can add the ``{field}``, ``{param}`` and ``{value}`` tags to your message, respectively:: @@ -534,6 +555,7 @@ Let's say we have a file with translations located here: **app/Languages/en/Rule We can simply use the language lines defined in this file, like this: .. literalinclude:: validation/025.php + :lines: 2- .. _validation-getting-all-errors: @@ -543,6 +565,7 @@ Getting All Errors If you need to retrieve all error messages for failed fields, you can use the ``getErrors()`` method: .. literalinclude:: validation/026.php + :lines: 2- If no errors exist, an empty array will be returned. @@ -573,6 +596,7 @@ You can retrieve the error for a single field with the ``getError()`` method. Th name: .. literalinclude:: validation/027.php + :lines: 2- If no error exists, an empty string will be returned. @@ -584,10 +608,12 @@ Check If Error Exists You can check to see if an error exists with the ``hasError()`` method. The only parameter is the field name: .. literalinclude:: validation/028.php + :lines: 2- When specifying a field with a wildcard, all errors matching the mask will be checked: .. literalinclude:: validation/029.php + :lines: 2- .. _validation-redirect-and-validation-errors: @@ -631,6 +657,7 @@ An array named ``$errors`` is available within the view that contains a list of the name of the field that had the error, and the value is the error message, like this: .. literalinclude:: validation/031.php + :lines: 2- There are actually two types of views that you can create. The first has an array of all of the errors, and is what we just looked at. The other type is simpler, and only contains a single variable, ``$error`` that contains the @@ -702,6 +729,7 @@ Using a Custom Rule Your new custom rule could now be used just like any other rule: .. literalinclude:: validation/036.php + :lines: 2- Allowing Parameters ------------------- @@ -727,6 +755,7 @@ you may use a closure instead of a rule class. You need to use an array for validation rules: .. literalinclude:: validation/040.php + :lines: 2- You must set the error message for the closure rule. When you specify the error message, set the array key for the closure rule. @@ -735,6 +764,7 @@ In the above code, the ``required`` rule has the key ``0``, and the closure has Or you can use the following parameters: .. literalinclude:: validation/041.php + :lines: 2- *************** Available Rules @@ -744,6 +774,7 @@ Available Rules There can be no spaces before and after ``ignore_value``. .. literalinclude:: validation/038.php + :lines: 2- Rules for General Use ===================== From cd911e7771a3d0fb7c2093b132533accee03d915 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 09:34:26 +0900 Subject: [PATCH 146/485] docs: change bold lines to section titles --- user_guide_src/source/libraries/validation.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index a3f968d72f27..ed1ce42b821b 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -462,14 +462,16 @@ See :ref:`validation-custom-errors` for details on the formatting of the array. Getting & Setting Rule Groups ----------------------------- -**Get Rule Group** +Get Rule Group +^^^^^^^^^^^^^^ This method gets a rule group from the validation configuration: .. literalinclude:: validation/017.php :lines: 2- -**Set Rule Group** +Set Rule Group +^^^^^^^^^^^^^^ This method sets a rule group from the validation configuration to the validation service: From 3c3227a608dab6edf95418ee1afac02d8ffb5f06 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 15:14:51 +0900 Subject: [PATCH 147/485] fix: Validation:check() parameter $rule type It should accept an array. --- system/Validation/Validation.php | 3 ++- user_guide_src/source/changelogs/v4.4.0.rst | 5 ++++- user_guide_src/source/installation/upgrade_440.rst | 5 ++++- user_guide_src/source/libraries/validation.rst | 8 +++++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index b4131af95305..118752c29355 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -201,9 +201,10 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup * determining whether validation was successful or not. * * @param array|bool|float|int|object|string|null $value + * @param array|string $rule * @param string[] $errors */ - public function check($value, string $rule, array $errors = []): bool + public function check($value, $rule, array $errors = []): bool { $this->reset(); diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index ccb48b873edc..df5b2c6da321 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -39,7 +39,10 @@ Interface Changes Method Signature Changes ======================== -- The third parameter ``Routing $routing`` has been added to ``RouteCollection::__construct()``. +- **Routing:** The third parameter ``Routing $routing`` has been added to + ``RouteCollection::__construct()``. +- **Validation:** The method signature of ``Validation::check()`` has been changed. + The ``string`` typehint on the ``$rule`` parameter was removed. Enhancements ************ diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 9d7d002d6efc..6f1a2b3d675c 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -78,9 +78,12 @@ The Cookie config items in **app/Config/App.php** are no longer used. Breaking Enhancements ********************* -- The method signature of ``RouteCollection::__construct()`` has been changed. +- **Routing:** The method signature of ``RouteCollection::__construct()`` has been changed. The third parameter ``Routing $routing`` has been added. Extending classes should likewise add the parameter so as not to break LSP. +- **Validation:** The method signature of ``Validation::check()`` has been changed. + The ``string`` typehint on the ``$rule`` parameter was removed. Extending classes + should likewise remove the typehint so as not to break LSP. Project Files ************* diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 2c9b5cb6994f..044d8769dc52 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -376,10 +376,16 @@ you previously set, so ``setRules()``, ``setRuleGroup()`` etc. need to be repeat Validating 1 Value ================== -Validate one value against a rule: +Validate one value against the rules: .. literalinclude:: validation/012.php +.. note:: Prior to v4.4.0, this method's second parameter, ``$rule``, was + typehinted to accept ``string``. In v4.4.0 and after, the typehint was + removed to allow arrays, too. + +.. note:: This method calls the ``setRule()`` method to set the rules internally. + .. _validation-getting-validated-data: Getting Validated Data From d1d6482d6623248cb46457e3fe511efcfdf9e8af Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 15:16:29 +0900 Subject: [PATCH 148/485] docs: improve sample code --- user_guide_src/source/libraries/validation/012.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/validation/012.php b/user_guide_src/source/libraries/validation/012.php index a43f9ab1a498..9b9e89c21c87 100644 --- a/user_guide_src/source/libraries/validation/012.php +++ b/user_guide_src/source/libraries/validation/012.php @@ -1,3 +1,5 @@ check($value, 'required'); +if ($validation->check($value, 'required')) { + // $value is valid. +} From a492f04f30ae1c8b67744ce0105067ebba00597f Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 15:25:36 +0900 Subject: [PATCH 149/485] docs: add doc comments --- system/Validation/Validation.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 118752c29355..71be2b698a55 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -200,9 +200,9 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup * Runs the validation process, returning true or false * determining whether validation was successful or not. * - * @param array|bool|float|int|object|string|null $value - * @param array|string $rule - * @param string[] $errors + * @param array|bool|float|int|object|string|null $value The data to validate. + * @param array|string $rule The validation rules. + * @param string[] $errors The custom error message. */ public function check($value, $rule, array $errors = []): bool { @@ -451,7 +451,8 @@ public function withRequest(RequestInterface $request): ValidationInterface * 'rule' => 'message', * ] * - * @param array|string $rules + * @param array|string $rules The validation rules. + * @param array $errors The custom error message. * * @return $this * From 2962b16473a1e6dcc67cd34b87ff4f4d280a3598 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 15:30:46 +0900 Subject: [PATCH 150/485] docs: add check() method parameter explanation --- user_guide_src/source/libraries/validation.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 044d8769dc52..331545a1478d 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -376,7 +376,10 @@ you previously set, so ``setRules()``, ``setRuleGroup()`` etc. need to be repeat Validating 1 Value ================== -Validate one value against the rules: +The ``check()`` method validates one value against the rules. +The first parameter ``$value`` is the value to validate. The second parameter +``$rule`` is the validation rules. +The optional third parameter ``$errors`` is the the custom error message. .. literalinclude:: validation/012.php From 3d6177c9a141aa023164ffccf45952577a67aaed Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 11:46:20 +0900 Subject: [PATCH 151/485] docs: change text decoration --- user_guide_src/source/tutorial/create_news_items.rst | 10 +++++----- user_guide_src/source/tutorial/index.rst | 2 +- user_guide_src/source/tutorial/news_section.rst | 8 ++++---- user_guide_src/source/tutorial/static_pages.rst | 10 +++++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/user_guide_src/source/tutorial/create_news_items.rst b/user_guide_src/source/tutorial/create_news_items.rst index 2c49a17c2f0a..d046dd8999c0 100644 --- a/user_guide_src/source/tutorial/create_news_items.rst +++ b/user_guide_src/source/tutorial/create_news_items.rst @@ -75,7 +75,7 @@ old input data when errors occur. Controller ========== -Go back to your **News** controller. You're going to do two things here, +Go back to your ``News`` controller. You're going to do two things here, check whether the form was submitted and whether the submitted data passed the validation rules. You'll use the :ref:`validation method in Controller ` to do this. @@ -105,7 +105,7 @@ above. You can read more about the :doc:`Validation library <../libraries/valida If the validation fails, the form is loaded and returned to display. -If the validation passed all the rules, the **NewsModel** is loaded and called. This +If the validation passed all the rules, the ``NewsModel`` is loaded and called. This takes care of passing the news item into the model. The :ref:`model-save` method handles inserting or updating the record automatically, based on whether it finds an array key matching the primary key. @@ -131,11 +131,11 @@ to allow data to be saved properly. The ``save()`` method that was used will determine whether the information should be inserted or if the row already exists and should be updated, based on the presence of a primary key. In this case, there is no ``id`` field passed to it, -so it will insert a new row into it's table, **news**. +so it will insert a new row into it's table, ``news``. However, by default the insert and update methods in the Model will not actually save any data because it doesn't know what fields are -safe to be updated. Edit the **NewsModel** to provide it a list of updatable +safe to be updated. Edit the ``NewsModel`` to provide it a list of updatable fields in the ``$allowedFields`` property. .. literalinclude:: create_news_items/003.php @@ -158,7 +158,7 @@ routing types in :doc:`../incoming/routing`. .. literalinclude:: create_news_items/004.php Now point your browser to your local development environment where you -installed CodeIgniter and add ``/news/create`` to the URL. +installed CodeIgniter and add **/news/create** to the URL. Add some news and check out the different pages you made. .. image:: ../images/tutorial3.png diff --git a/user_guide_src/source/tutorial/index.rst b/user_guide_src/source/tutorial/index.rst index 85675764eceb..830ab6568f9a 100644 --- a/user_guide_src/source/tutorial/index.rst +++ b/user_guide_src/source/tutorial/index.rst @@ -76,7 +76,7 @@ Setting Development Mode By default, CodeIgniter starts up in production mode. This is a safety feature to keep your site a bit more secure in case settings are messed up once it is live. -So first let's fix that. Copy or rename the ``env`` file to ``.env``. Open it up. +So first let's fix that. Copy or rename the **env** file to **.env**. Open it up. This file contains server-specific settings. This means you never will need to commit any sensitive information to your version control system. It includes diff --git a/user_guide_src/source/tutorial/news_section.rst b/user_guide_src/source/tutorial/news_section.rst index 9fa090701f67..f9a47879c328 100644 --- a/user_guide_src/source/tutorial/news_section.rst +++ b/user_guide_src/source/tutorial/news_section.rst @@ -51,7 +51,7 @@ The seed records might be something like:: Connect to Your Database ************************ -The local configuration file, ``.env``, that you created when you installed +The local configuration file, **.env**, that you created when you installed CodeIgniter, should have the database property settings uncommented and set appropriately for the database you want to use. Make sure you've configured your database properly as described in :doc:`../database/configuration`:: @@ -101,7 +101,7 @@ query; :doc:`Query Builder <../database/query_builder>` does this for you. The two methods used here, ``findAll()`` and ``first()``, are provided by the ``CodeIgniter\Model`` class. They already know the table to use based on the ``$table`` -property we set in **NewsModel** class, earlier. They are helper methods +property we set in ``NewsModel`` class, earlier. They are helper methods that use the Query Builder to run their commands on the current table, and returning an array of results in the format of your choice. In this example, ``findAll()`` returns an array of array. @@ -126,7 +126,7 @@ access to the current ``Request`` and ``Response`` objects, as well as the Next, there are two methods, one to view all news items, and one for a specific news item. -Next, the :php:func:`model()` function is used to create the **NewsModel** instance. +Next, the :php:func:`model()` function is used to create the ``NewsModel`` instance. This is a helper function. You can read more about it in :doc:`../general/common_functions`. You could also write ``$model = new NewsModel();``, if you don't use it. @@ -187,7 +187,7 @@ with a slug to the ``view()`` method in the ``News`` controller. .. literalinclude:: news_section/008.php -Point your browser to your "news" page, i.e., ``localhost:8080/news``, +Point your browser to your "news" page, i.e., **localhost:8080/news**, you should see a list of the news items, each of which has a link to display just the one article. diff --git a/user_guide_src/source/tutorial/static_pages.rst b/user_guide_src/source/tutorial/static_pages.rst index d39224f3b018..3730063c5fef 100644 --- a/user_guide_src/source/tutorial/static_pages.rst +++ b/user_guide_src/source/tutorial/static_pages.rst @@ -160,7 +160,7 @@ arguments. More information about routing can be found in the :doc:`../incoming/routing`. Here, the second rule in the ``$routes`` object matches a GET request -to the URI path ``/pages``, and it maps to the ``index()`` method of the ``Pages`` class. +to the URI path **/pages**, and it maps to the ``index()`` method of the ``Pages`` class. The third rule in the ``$routes`` object matches a GET request to a URI segment using the placeholder ``(:segment)``, and passes the parameter to the @@ -170,8 +170,8 @@ Running the App *************** Ready to test? You cannot run the app using PHP's built-in server, -since it will not properly process the ``.htaccess`` rules that are provided in -``public``, and which eliminate the need to specify "**index.php/**" +since it will not properly process the **.htaccess** rules that are provided in +**public**, and which eliminate the need to specify "**index.php/**" as part of a URL. CodeIgniter has its own command that you can use though. From the command line, at the root of your project:: @@ -179,9 +179,9 @@ From the command line, at the root of your project:: > php spark serve will start a web server, accessible on port 8080. If you set the location field -in your browser to ``localhost:8080``, you should see the CodeIgniter welcome page. +in your browser to **localhost:8080**, you should see the CodeIgniter welcome page. -Now visit ``localhost:8080/home``. Did it get routed correctly to the ``view()`` +Now visit **localhost:8080/home**. Did it get routed correctly to the ``view()`` method in the ``Pages`` controller? Awesome! You should see something like the following: From fba7fce906febda2b0997ed3f4ed4b1cb22d3ac3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 11:46:55 +0900 Subject: [PATCH 152/485] docs: remove / after folder names --- user_guide_src/source/tutorial/news_section.rst | 2 +- user_guide_src/source/tutorial/static_pages.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/tutorial/news_section.rst b/user_guide_src/source/tutorial/news_section.rst index f9a47879c328..942ed408bfbb 100644 --- a/user_guide_src/source/tutorial/news_section.rst +++ b/user_guide_src/source/tutorial/news_section.rst @@ -71,7 +71,7 @@ are the place where you retrieve, insert, and update information in your database or other data stores. They provide access to your data. You can read more about it in :doc:`../models/model`. -Open up the **app/Models/** directory and create a new file called +Open up the **app/Models** directory and create a new file called **NewsModel.php** and add the following code. .. literalinclude:: news_section/001.php diff --git a/user_guide_src/source/tutorial/static_pages.rst b/user_guide_src/source/tutorial/static_pages.rst index 3730063c5fef..ea9129518eed 100644 --- a/user_guide_src/source/tutorial/static_pages.rst +++ b/user_guide_src/source/tutorial/static_pages.rst @@ -82,7 +82,7 @@ Adding Logic to the Controller Earlier you set up a controller with a ``view()`` method. The method accepts one parameter, which is the name of the page to be loaded. The -static page bodies will be located in the **app/Views/pages/** +static page bodies will be located in the **app/Views/pages** directory. In that directory, create two files named **home.php** and **about.php**. From 082377bc0ecc420af813dae3e7a186a41abc5e70 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 14:52:29 +0900 Subject: [PATCH 153/485] docs: update tutorial - add News::new() to show HTML form - change News::view() to show() - change routing --- .../source/tutorial/create_news_items.rst | 93 ++++++++++++------- .../source/tutorial/create_news_items/002.php | 34 +------ .../source/tutorial/create_news_items/004.php | 8 +- .../source/tutorial/create_news_items/005.php | 40 ++++++++ .../source/tutorial/news_section.rst | 12 +-- .../source/tutorial/news_section/003.php | 2 +- .../source/tutorial/news_section/006.php | 2 +- .../source/tutorial/news_section/008.php | 9 +- .../source/tutorial/static_pages.rst | 7 +- .../source/tutorial/static_pages/003.php | 7 +- 10 files changed, 128 insertions(+), 86 deletions(-) create mode 100644 user_guide_src/source/tutorial/create_news_items/005.php diff --git a/user_guide_src/source/tutorial/create_news_items.rst b/user_guide_src/source/tutorial/create_news_items.rst index d046dd8999c0..d44267062142 100644 --- a/user_guide_src/source/tutorial/create_news_items.rst +++ b/user_guide_src/source/tutorial/create_news_items.rst @@ -3,7 +3,7 @@ Create News Items .. contents:: :local: - :depth: 2 + :depth: 3 You now know how you can read data from a database using CodeIgniter, but you haven't written any information to the database yet. In this section, @@ -35,15 +35,16 @@ View To input data into the database, you need to create a form where you can input the information to be stored. This means you'll be needing a form with two fields, one for the title and one for the text. You'll derive -the slug from our title in the model. Create a new view at -**app/Views/news/create.php**:: +the slug from our title in the model. + +Create a new view at **app/Views/news/create.php**::

getFlashdata('error') ?> -
+ @@ -75,40 +76,58 @@ old input data when errors occur. Controller ========== -Go back to your ``News`` controller. You're going to do two things here, -check whether the form was submitted and whether the submitted data -passed the validation rules. -You'll use the :ref:`validation method in Controller ` to do this. +Go back to your ``News`` controller. + +Create a Method to Display the Form +----------------------------------- + +First, create a method to display the HTML form you have created. .. literalinclude:: create_news_items/002.php -The code above adds a lot of functionality. +We load the :doc:`Form helper <../helpers/form_helper>` with the +:php:func:`helper()` function. Most helper functions require the helper to be +loaded before use. + +Then it returns the created form view. + +Create a Method to Create a News Item +------------------------------------- + +Next, create a method to create a news item from the submitted data. + +You're going to do three things here: + +1. checks whether the submitted data passed the validation rules. +2. saves the news item to the database. +3. returns a success page. -First we load the :doc:`Form helper <../helpers/form_helper>` with the :php:func:`helper()` function. -Most helper functions require the helper to be loaded before use. +.. literalinclude:: create_news_items/005.php -Next, we check if we deal with the **POST** request with the -:doc:`IncomingRequest <../incoming/incomingrequest>` object ``$this->request``. -It is set in the controller by the framework. -The :ref:`IncomingRequest::is() ` method checks the type of the request. -Since the route for **create()** endpoint handles both: **GET** and **POST** requests we can safely assume that if the request is not POST then it is a GET type. -the form is loaded and returned to display. +The code above adds a lot of functionality. -Then, we get the necessary items from the POST data by the user and set them in the ``$post`` variable. -We also use the :doc:`IncomingRequest <../incoming/incomingrequest>` object ``$this->request``. +Validate the Data +^^^^^^^^^^^^^^^^^ -After that, the Controller-provided helper function :ref:`validateData() ` -is used to validate ``$post`` data. +You'll use the Controller-provided helper function :ref:`validate() ` to validate the submitted data. In this case, the title and body fields are required and in the specific length. CodeIgniter has a powerful validation library as demonstrated above. You can read more about the :doc:`Validation library <../libraries/validation>`. -If the validation fails, the form is loaded and returned to display. +If the validation fails, we call the ``new()`` method you just created and return +the HTML form. + +Save the News Item +^^^^^^^^^^^^^^^^^^ + +If the validation passed all the rules, we get the validated data by +:ref:`$this->validator->getValidated() ` and +set them in the ``$post`` variable. -If the validation passed all the rules, the ``NewsModel`` is loaded and called. This -takes care of passing the news item into the model. The :ref:`model-save` method handles -inserting or updating the record automatically, based on whether it finds an array key -matching the primary key. +The ``NewsModel`` is loaded and called. This takes care of passing the news item +into the model. The :ref:`model-save` method handles inserting or updating the +record automatically, based on whether it finds an array key matching the primary +key. This contains a new function :php:func:`url_title()`. This function - provided by the :doc:`URL helper <../helpers/url_helper>` - strips down @@ -116,8 +135,11 @@ the string you pass it, replacing all spaces by dashes (``-``) and makes sure everything is in lowercase characters. This leaves you with a nice slug, perfect for creating URIs. -After this, view files are loaded and returned to display a success message. Create a view at -**app/Views/news/success.php** and write a success message. +Return Success Page +^^^^^^^^^^^^^^^^^^^ + +After this, view files are loaded and returned to display a success message. +Create a view at **app/Views/news/success.php** and write a success message. This could be as simple as:: @@ -151,12 +173,21 @@ Routing Before you can start adding news items into your CodeIgniter application you have to add an extra rule to **app/Config/Routes.php** file. Make sure your -file contains the following. This makes sure CodeIgniter sees ``create()`` -as a method instead of a news item's slug. You can read more about different -routing types in :doc:`../incoming/routing`. +file contains the following: .. literalinclude:: create_news_items/004.php +The route directive for ``'news/new'`` is placed before the directive for ``'news/(:segment)'`` to ensure that the form to create a news item is displayed. + +The ``$routes->post()`` line defines the router for a POST request. It matches +only a POST request to the URI path **/news**, and it maps to the ``create()`` method of +the ``News`` class. + +You can read more about different routing types in :ref:`defined-route-routing`. + +Create a News Item +****************** + Now point your browser to your local development environment where you installed CodeIgniter and add **/news/create** to the URL. Add some news and check out the different pages you made. diff --git a/user_guide_src/source/tutorial/create_news_items/002.php b/user_guide_src/source/tutorial/create_news_items/002.php index be1ad6a54836..0b196dfa7245 100644 --- a/user_guide_src/source/tutorial/create_news_items/002.php +++ b/user_guide_src/source/tutorial/create_news_items/002.php @@ -3,46 +3,18 @@ namespace App\Controllers; use App\Models\NewsModel; +use CodeIgniter\Exceptions\PageNotFoundException; class News extends BaseController { // ... - public function create() + public function new() { helper('form'); - // Checks whether the form is submitted. - if (! $this->request->is('post')) { - // The form is not submitted, so returns the form. - return view('templates/header', ['title' => 'Create a news item']) - . view('news/create') - . view('templates/footer'); - } - - $post = $this->request->getPost(['title', 'body']); - - // Checks whether the submitted data passed the validation rules. - if (! $this->validateData($post, [ - 'title' => 'required|max_length[255]|min_length[3]', - 'body' => 'required|max_length[5000]|min_length[10]', - ])) { - // The validation fails, so returns the form. - return view('templates/header', ['title' => 'Create a news item']) - . view('news/create') - . view('templates/footer'); - } - - $model = model(NewsModel::class); - - $model->save([ - 'title' => $post['title'], - 'slug' => url_title($post['title'], '-', true), - 'body' => $post['body'], - ]); - return view('templates/header', ['title' => 'Create a news item']) - . view('news/success') + . view('news/create') . view('templates/footer'); } } diff --git a/user_guide_src/source/tutorial/create_news_items/004.php b/user_guide_src/source/tutorial/create_news_items/004.php index 3de06f181d29..6b04f3c66ffd 100644 --- a/user_guide_src/source/tutorial/create_news_items/004.php +++ b/user_guide_src/source/tutorial/create_news_items/004.php @@ -5,10 +5,10 @@ use App\Controllers\News; use App\Controllers\Pages; -$routes->match(['get', 'post'], 'news/create', [News::class, 'create']); -$routes->get('news/(:segment)', [News::class, 'view']); $routes->get('news', [News::class, 'index']); +$routes->get('news/new', [News::class, 'new']); // Add this line +$routes->post('news', [News::class, 'create']); // Add this line +$routes->get('news/(:segment)', [News::class, 'show']); + $routes->get('pages', [Pages::class, 'index']); $routes->get('(:segment)', [Pages::class, 'view']); - -// ... diff --git a/user_guide_src/source/tutorial/create_news_items/005.php b/user_guide_src/source/tutorial/create_news_items/005.php new file mode 100644 index 000000000000..6d565789141a --- /dev/null +++ b/user_guide_src/source/tutorial/create_news_items/005.php @@ -0,0 +1,40 @@ +validate([ + 'title' => 'required|max_length[255]|min_length[3]', + 'body' => 'required|max_length[5000]|min_length[10]', + ])) { + // The validation fails, so returns the form. + return $this->new(); + } + + // Gets the validated data. + $post = $this->validator->getValidated(); + + $model = model(NewsModel::class); + + $model->save([ + 'title' => $post['title'], + 'slug' => url_title($post['title'], '-', true), + 'body' => $post['body'], + ]); + + return view('templates/header', ['title' => 'Create a news item']) + . view('news/success') + . view('templates/footer'); + } +} diff --git a/user_guide_src/source/tutorial/news_section.rst b/user_guide_src/source/tutorial/news_section.rst index 942ed408bfbb..0c32f6f92626 100644 --- a/user_guide_src/source/tutorial/news_section.rst +++ b/user_guide_src/source/tutorial/news_section.rst @@ -162,7 +162,7 @@ The news overview page is now done, but a page to display individual news items is still absent. The model created earlier is made in such a way that it can easily be used for this functionality. You only need to add some code to the controller and create a new view. Go back to the -``News`` controller and update the ``view()`` method with the following: +``News`` controller and update the ``show()`` method with the following: .. literalinclude:: news_section/006.php @@ -179,14 +179,14 @@ The only thing left to do is create the corresponding view at Routing ******* -Modify your routing file -(**app/Config/Routes.php**) so it looks as follows. -This makes sure the requests reach the ``News`` controller instead of -going directly to the ``Pages`` controller. The first line routes URI's -with a slug to the ``view()`` method in the ``News`` controller. +Modify your **app/Config/Routes.php** file, so it looks as follows: .. literalinclude:: news_section/008.php +This makes sure the requests reach the ``News`` controller instead of +going directly to the ``Pages`` controller. The second ``$routes->get()`` line +routes URI's with a slug to the ``show()`` method in the ``News`` controller. + Point your browser to your "news" page, i.e., **localhost:8080/news**, you should see a list of the news items, each of which has a link to display just the one article. diff --git a/user_guide_src/source/tutorial/news_section/003.php b/user_guide_src/source/tutorial/news_section/003.php index 13c3b7263db6..22b24577b01e 100644 --- a/user_guide_src/source/tutorial/news_section/003.php +++ b/user_guide_src/source/tutorial/news_section/003.php @@ -13,7 +13,7 @@ public function index() $data['news'] = $model->getNews(); } - public function view($slug = null) + public function show($slug = null) { $model = model(NewsModel::class); diff --git a/user_guide_src/source/tutorial/news_section/006.php b/user_guide_src/source/tutorial/news_section/006.php index 657211792f46..95e48aa77586 100644 --- a/user_guide_src/source/tutorial/news_section/006.php +++ b/user_guide_src/source/tutorial/news_section/006.php @@ -9,7 +9,7 @@ class News extends BaseController { // ... - public function view($slug = null) + public function show($slug = null) { $model = model(NewsModel::class); diff --git a/user_guide_src/source/tutorial/news_section/008.php b/user_guide_src/source/tutorial/news_section/008.php index 077efd42254d..df11598451a1 100644 --- a/user_guide_src/source/tutorial/news_section/008.php +++ b/user_guide_src/source/tutorial/news_section/008.php @@ -2,12 +2,11 @@ // ... -use App\Controllers\News; +use App\Controllers\News; // Add this line use App\Controllers\Pages; -$routes->get('news/(:segment)', [News::class, 'view']); -$routes->get('news', [News::class, 'index']); +$routes->get('news', [News::class, 'index']); // Add this line +$routes->get('news/(:segment)', [News::class, 'show']); // Add this line + $routes->get('pages', [Pages::class, 'index']); $routes->get('(:segment)', [Pages::class, 'view']); - -// ... diff --git a/user_guide_src/source/tutorial/static_pages.rst b/user_guide_src/source/tutorial/static_pages.rst index ea9129518eed..dad499b2a806 100644 --- a/user_guide_src/source/tutorial/static_pages.rst +++ b/user_guide_src/source/tutorial/static_pages.rst @@ -135,17 +135,16 @@ Routing We have made the controller. The next thing is to set routing rules. Routing associates a URI with a controller's method. -Let's do that. Open the routing file located at -**app/Config/Routes.php**. +Let's do that. Open the routes file located at **app/Config/Routes.php**. -The only line there to start with should be: +The only route directive there to start with should be: .. literalinclude:: static_pages/003.php This directive says that any incoming request without any content specified should be handled by the ``index()`` method inside the ``Home`` controller. -Add the following lines, **after** the route directive for '/'. +Add the following lines, **after** the route directive for ``'/'``. .. literalinclude:: static_pages/004.php :lines: 2- diff --git a/user_guide_src/source/tutorial/static_pages/003.php b/user_guide_src/source/tutorial/static_pages/003.php index bf0466ca2192..fc4914a6923b 100644 --- a/user_guide_src/source/tutorial/static_pages/003.php +++ b/user_guide_src/source/tutorial/static_pages/003.php @@ -1,7 +1,8 @@ get('/', 'Home::index'); - -// ... From 28825f20555288af3e741b3cc40671614474d02a Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 15:03:22 +0900 Subject: [PATCH 154/485] docs: extract sample code to PHP file --- .../source/tutorial/create_news_items.rst | 21 ++----------------- .../source/tutorial/create_news_items/006.php | 18 ++++++++++++++++ 2 files changed, 20 insertions(+), 19 deletions(-) create mode 100644 user_guide_src/source/tutorial/create_news_items/006.php diff --git a/user_guide_src/source/tutorial/create_news_items.rst b/user_guide_src/source/tutorial/create_news_items.rst index d44267062142..068d41966c1c 100644 --- a/user_guide_src/source/tutorial/create_news_items.rst +++ b/user_guide_src/source/tutorial/create_news_items.rst @@ -37,26 +37,9 @@ input the information to be stored. This means you'll be needing a form with two fields, one for the title and one for the text. You'll derive the slug from our title in the model. -Create a new view at **app/Views/news/create.php**:: +Create a new view at **app/Views/news/create.php**: -

- - getFlashdata('error') ?> - - - - - - - -
- - - -
- - - +.. literalinclude:: create_news_items/006.php There are probably only four things here that look unfamiliar. diff --git a/user_guide_src/source/tutorial/create_news_items/006.php b/user_guide_src/source/tutorial/create_news_items/006.php new file mode 100644 index 000000000000..b19bbfe9b2c5 --- /dev/null +++ b/user_guide_src/source/tutorial/create_news_items/006.php @@ -0,0 +1,18 @@ +

+ +getFlashdata('error') ?> + + +
+ + + + +
+ + + +
+ + +
From 999a069822b5161fc4b6802250916638aba0865d Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 15:42:07 +0900 Subject: [PATCH 155/485] docs: add or update section titles --- .../source/tutorial/create_news_items.rst | 22 ++++++------ .../source/tutorial/news_section.rst | 36 +++++++++++++++---- .../source/tutorial/static_pages.rst | 22 +++++++++--- 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/user_guide_src/source/tutorial/create_news_items.rst b/user_guide_src/source/tutorial/create_news_items.rst index 068d41966c1c..f6c7b1050af8 100644 --- a/user_guide_src/source/tutorial/create_news_items.rst +++ b/user_guide_src/source/tutorial/create_news_items.rst @@ -29,8 +29,8 @@ You can read more about the CSRF protection in :doc:`Security <../libraries/secu Create a Form ************* -View -==== +Create news/create View File +============================ To input data into the database, you need to create a form where you can input the information to be stored. This means you'll be needing a form @@ -56,12 +56,12 @@ The :php:func:`csrf_field()` function creates a hidden input with a CSRF token t The :php:func:`set_value()` function provided by the :doc:`../helpers/form_helper` is used to show old input data when errors occur. -Controller -========== +News Controller +=============== Go back to your ``News`` controller. -Create a Method to Display the Form +Add News::new() to Display the Form ----------------------------------- First, create a method to display the HTML form you have created. @@ -74,8 +74,8 @@ loaded before use. Then it returns the created form view. -Create a Method to Create a News Item -------------------------------------- +Add News::create() to Create a News Item +---------------------------------------- Next, create a method to create a news item from the submitted data. @@ -128,8 +128,8 @@ This could be as simple as::

News item created successfully.

-Model Updating -************** +NewsModel Updating +****************** The only thing that remains is ensuring that your model is set up to allow data to be saved properly. The ``save()`` method that was @@ -151,8 +151,8 @@ never need to do that, since it is an auto-incrementing field in the database. This helps protect against Mass Assignment Vulnerabilities. If your model is handling your timestamps, you would also leave those out. -Routing -******* +Adding Routing Rules +******************** Before you can start adding news items into your CodeIgniter application you have to add an extra rule to **app/Config/Routes.php** file. Make sure your diff --git a/user_guide_src/source/tutorial/news_section.rst b/user_guide_src/source/tutorial/news_section.rst index 0c32f6f92626..183b2f0c8570 100644 --- a/user_guide_src/source/tutorial/news_section.rst +++ b/user_guide_src/source/tutorial/news_section.rst @@ -71,6 +71,9 @@ are the place where you retrieve, insert, and update information in your database or other data stores. They provide access to your data. You can read more about it in :doc:`../models/model`. +Create NewsModel +================ + Open up the **app/Models** directory and create a new file called **NewsModel.php** and add the following code. @@ -81,6 +84,9 @@ creates a new model by extending ``CodeIgniter\Model`` and loads the database library. This will make the database class available through the ``$this->db`` object. +Add NewsModel::getNews() Method +=============================== + Now that the database and a model have been set up, you'll need a method to get all of our posts from our database. To do this, the database abstraction layer that is included with CodeIgniter - @@ -112,8 +118,12 @@ Display the News Now that the queries are written, the model should be tied to the views that are going to display the news items to the user. This could be done in our ``Pages`` controller created earlier, but for the sake of clarity, -a new ``News`` controller is defined. Create the new controller at -**app/Controllers/News.php**. +a new ``News`` controller is defined. + +Create News Controller +====================== + +Create the new controller at **app/Controllers/News.php**. .. literalinclude:: news_section/003.php @@ -134,6 +144,9 @@ You can see that the ``$slug`` variable is passed to the model's method in the second method. The model is using this slug to identify the news item to be returned. +Complete News::index() Method +============================= + Now the data is retrieved by the controller through our model, but nothing is displayed yet. The next thing to do is, passing this data to the views. Modify the ``index()`` method to look like this: @@ -143,8 +156,12 @@ the views. Modify the ``index()`` method to look like this: The code above gets all news records from the model and assigns it to a variable. The value for the title is also assigned to the ``$data['title']`` element and all data is passed to the views. You now need to create a -view to render the news items. Create **app/Views/news/index.php** -and add the next piece of code. +view to render the news items. + +Create news/index View File +=========================== + +Create **app/Views/news/index.php** and add the next piece of code. .. literalinclude:: news_section/005.php @@ -158,6 +175,9 @@ wrote our template in PHP mixed with HTML. If you prefer to use a template language, you can use CodeIgniter's :doc:`View Parser ` or a third party parser. +Complete News::show() Method +============================ + The news overview page is now done, but a page to display individual news items is still absent. The model created earlier is made in such a way that it can easily be used for this functionality. You only need to @@ -171,13 +191,17 @@ the ``PageNotFoundException`` class. Instead of calling the ``getNews()`` method without a parameter, the ``$slug`` variable is passed, so it will return the specific news item. + +Create news/view View File +========================== + The only thing left to do is create the corresponding view at **app/Views/news/view.php**. Put the following code in this file. .. literalinclude:: news_section/007.php -Routing -******* +Adding Routing Rules +******************** Modify your **app/Config/Routes.php** file, so it looks as follows: diff --git a/user_guide_src/source/tutorial/static_pages.rst b/user_guide_src/source/tutorial/static_pages.rst index dad499b2a806..25b514b51c4b 100644 --- a/user_guide_src/source/tutorial/static_pages.rst +++ b/user_guide_src/source/tutorial/static_pages.rst @@ -16,6 +16,9 @@ It is the glue of your web application. Let's Make our First Controller ******************************* +Create Pages Controller +======================= + Create a file at **app/Controllers/Pages.php** with the following code. @@ -47,6 +50,9 @@ The **controller is what will become the center of every request** to your web application. Like any PHP class, you refer to it within your controllers as ``$this``. +Create Views +============ + Now that you've created your first method, it's time to make some basic page templates. We will be creating two "views" (page templates) that act as our page footer and header. @@ -80,15 +86,21 @@ includes the following code:: Adding Logic to the Controller ****************************** +Create home.php and about.php +============================= + Earlier you set up a controller with a ``view()`` method. The method -accepts one parameter, which is the name of the page to be loaded. The -static page bodies will be located in the **app/Views/pages** -directory. +accepts one parameter, which is the name of the page to be loaded. + +The static page bodies will be located in the **app/Views/pages** directory. In that directory, create two files named **home.php** and **about.php**. Within those files, type some text - anything you'd like - and save them. If you like to be particularly un-original, try "Hello World!". +Complete Pages::view() Method +============================= + In order to load those pages, you'll have to check whether the requested page actually exists. This will be the body of the ``view()`` method in the ``Pages`` controller created above: @@ -129,8 +141,8 @@ view. throw errors on case-sensitive platforms. You can read more about it in :doc:`../outgoing/views`. -Routing -******* +Setting Routing Rules +********************* We have made the controller. The next thing is to set routing rules. Routing associates a URI with a controller's method. From 03a9b9f191e16c46955ac9044003667555e1c17c Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 16:23:15 +0900 Subject: [PATCH 156/485] docs: remove app image and add text diagram --- user_guide_src/source/images/tutorial9.png | Bin 45046 -> 0 bytes .../source/tutorial/create_news_items.rst | 32 ++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) delete mode 100644 user_guide_src/source/images/tutorial9.png diff --git a/user_guide_src/source/images/tutorial9.png b/user_guide_src/source/images/tutorial9.png deleted file mode 100644 index 39986162c65b3f93ca203347ea30cf7b5685020c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45046 zcmb5W1ymi)(k_f_VB@YqHX7XB-5r8Mkl=2?-CY8~gIjPXSa62`NpJ`*!QJ5w?>XQ1 zo%R3s-nA}kZFVy=-7{U&UG-GeQ$(pM%b+3=AwfYwp~}fhssrDbP*5-#V0fUFun4aK z1qCf@Eg_*QCm{h*b#b(?wljxC@lcP9j8!)d810_u z9_=3J?(P1PZeXxRfUyRv*$Y*u*3)8(5(~MnXCAJc;!ZL*n6bRE@GwMQxRrMhDe=bX zPEFR!Mk#uMa@KPZQ$vJ>HA70QGB8qyGWCF3ppX)kfnIPGJ#+*6z((ePOvs=s5D{JA z>m8vMU`7a|L^%lFLoM*j$!_e++Ld)ex;KgP4>nCd*qIz+DRzG=3Q7JIjfHWP{a#f! zCF6@)!5SE)SQG9?#|I%2I7(Pb33zzbbP3G>JlF{8!g;z-9ZWLVkSjNM_>u~6oo4`i zxcvzsfusP2=~rA7l#bshDD9K@RCv_AB;B`h#k>>q;NBfNQ)A<&vzHf3U3mCwLll&N znCF)lp_`YN?+OG2>5lBI#Izu&U##zaheN)n0;1J2*O9YOQi7rf+F&T?2x}-1(1Hd& zM8F3M3N|?$3IX`W0zQ&?F#mn_G7t8@+AtZf7m8^}$jJfU8fGr$<_@k_j&8Y2n_a+A zv(}nAZaPYe{AP~!%qAZkP0g7-?VVn`KnZ&C15JB#Hxr1by`6(Azo!tzKUeSr?bpLB z6p()|akCYo&{0x_NI1HfL%5hZnOP}>ksuI=pvwmfesxLde|HD|5~8qjb93TnVe#pAle~toj7Df_e`R{iojHDtTuL1=n3MD5grs)ZNXo%p8 z^BbGgMjYJf0VbhSeU)~qt86l&Veve-*ohT)h=4U|&4rHM; z)<@1+6+vwBq=5TPYn5f0%R+xbo_{KZB)B(L6Q-o*$kJK6Uf*p_XA`eQtCFe7ol^bwd~*T$BsP_ znx{YMmOEYlay#}Wc&b}RnR`#KLPVd<#5J+#9nY^lqE!BrJY6r8wl{pY8bjsh}T`83Q{@cgm|-I?!J;q}7`tXxt&6H3z$rvpnEH`gPQsC;FUICETVPGC zTgE1m&YTiEL5TrZc=ZQM7Z>E25_x5Gvhbgl4d~Nl=5O~*Jb4*f9gLpGp&JjjQ2z>< zizvPGay7p!6-EuYG+#i1L5vp7*0);D(XydE@3^6#NkVSHmdu#`{2PH;qxktiC+Ohw zq_|vahJPLoZvzpGXkK?8KIx_#?_|bmLT1WXyy52ju==z@sBFlUjX+3m|aK0)f=IjkLn={Oc=I()h1 zbNS)?_%AS0kK4nI9^85K%~zCPQbx)$h+MTt#hm?Id=1dtB;d{rD6or=4 zUd(dwVSc{4*t##NLXb?A*9S78wdss0){O+d%@#Mqsw~J{KhC4+!LVr7S8`fG^XY!r zOH6D>-|LNI_0D&3_yQ;5N1Y$>nWsIS&z)KyV-; z^b&1j3tr@*u=}UKC`*#*N8uG6SGVr4@vErF^`NWCX|7zW)ByPv9s z+^~h-b5gFTqX!?oCEZR)QAGmapik>IZA61pg3L^^_eweoYI zJFn)skhE*63`S;z@adW|Ik7Q8mC>SDCX979bF%HApmcIbEHzu7f;uwHXBkM83!C9X z`e(;3PE`yH5GTyb<3)l~v^DDo1~3&oXv4i&#}rEOSPY|<9j0JP>zKqk3xXj5@;z?) zeOO#UDTcFlA|n-YuJ*jlN0E~TE7lzko*xx|-~KVq|K-V}%}*d$tp82mA4Jqbk_WWu zp3S#H^vA26KAg6*jQ4wm340AcgbOFKcziVD8x+UVSV~>CN2m22I|ptI@(|Q~isGO*0k4(j{B2;lqOF^=ExER788GL;3d*b69w|`rY~V{-1!C zVj88)sSFNFtL{MP%0Nn)cwa_8MU_UI-_gRiYp^zdx`e1zzGSBgC}nc8F-gxnV8Arj zPpKNyvV-K0loXESaswXEhN4%t$l(wW8g^@Ke$y|uI#b3Ga)h_EjU%9w*3P_dbrQ?n zILZ<5*n56@z`?>2ABrd0WMW=Z+E{3?+`qp%_*HMNF#4J4&E4JIG|Tr#@oz5M?4#Z8 zTM~q)II8iHj+Dj>Vba>VQswzYB9EuAem5%~Da?lao7$S?>eN~eex^6aD=CyRXrs9z zA`BL(^zXpYDEq(4v9I=jMzDVv7KKB`+d8^FTH0U!=99h|$j`3pmM=3C5U9!jVat1KJA@smEJPuBo z!113gfBbEWh9-ML5WL2P9ajY%`Y!i>?w98JN2f@m`C>!s({mae5pr0lerD7y6AOVy zoN+ZV!Swy(6lREsb2VF~|Lc1|K=X#z`4)L>mF15w_8;yZFMiNDu6Cr*zoY*e_l$(*|)<6yN(xYs#+-e)pHsA8Bg@7@UDS$fcigBO7=F`&|pk9@l*E z2mIzvZy55=n(xo|^EG?hY40u2DSWOE1+9y#_^^6y%zU#Y>*BoWRd)tr@MYAWLy5dS zz0s3d958BA#m#8&@tDY&8os@ejw2d4zSMs2H&9cVS6S0_Pp|YvFyo8gwQlod`r+Tj zCWfPx_B3GXGAnKFg@6E;3>0H;4rZ&y8g}fLnyF)R-}@YK=`~uZ#f~MRM4?0L15^73 zyZ|G=7ve1DFjJ)r_Aq#K((#&4tdyw50~=W5c5jt3Q$9244{Ck;Fp{iUrN{MEqg1)* z%t4W{Ng4SBs%GdkP-3ObcD~l6?i=n%mMli%sT_+N)vDC{Z!TeEoZjqXb@S~WC+9kg zf?qx=ZXUD!z28g_@w_%jn=4kpAI%ZqK_%v$S}~zAG|5Px=eW)2b5`#n6yXo11 zx76m&V87TH1*}OKF)=aj4od5K9t@1IYxrnHO?bRgW7NB|%^^f&Q z&|?%3B>RQxq;NzSHkocMK%IT>9C8Jh&TpmzP<3`<)W(Gw)fxwBkg}1eM*&#Cd4vxBYd#=cwibe6FuAH?zXB!c{Y#*yqY2W zWnjdQSAXAyXjH1^i-WYV!L36Wwx}*)5HXFgYp#c!@}cQZJxv4Vu;0d+tI=)y)mX&ER>(~v>oHx}CV@GK3nat# z22)eH${Dysn~$d_jc1$gfaz%oE6?)8LiQ$j{!r0UF>*386Nn$o)sPkUk}qTX{)s>* zvmkBo_~Vx*YF<>u)+^M*9PNDy1^t#+oD=hFly}~vF%@nd4KOLIZcDAdz+U`H?yhA#yh6kJ=TD%>wgCghzmf^GP;T%w#aszp%EH6xypUzVxVXf_n@ zi*|t-`F#-2j`j_=-P#kd4G335D)_g2d&vQ139qPEu8=~Tls8U|I6zmjS;e+GaEqV4 zqWAdpw7?YJ^pRS1`H%lYTww!7K3p%j^@^ZNbe!0d7ybch`683cR^8Yb!clEy8-GAC zE8hJP3H3Jb7r}u^mMY8`E?Kr|^1!SRt%Q z0c_R^@$&pr{boCpQOei~smiE8Cfr4O)j{e~f^=E+62%d=AF7&`!HD#VPC@bD&}Rbw zz#06r>FOfG_KzTO|JG>&oCWg$ekg?99QQVj2pCnLa=5-+*90NsZyC45Q6E|!MY((Kn)h{c*P87@J*Uj z;C^%%_V9SJ>|#7Ih!L>87_NxKs$iq9_=$C3t@W@Io$=e}zJ&cs=iD6`?dL^fdMHoT$@$-%1$~ z$@uA}kjA`?K1nWX`ttnL5sFQobU&7?%;B|81tTyRPomIZ`J-fQ%{aa-nx6O<^1HTo zppEZmN6eBz88<7Vhhhb{WqlzQ1H5B*7rSzUQ*7Z(6n!}l=nL!>?~yu5;sN0NPZ^U+fm1eXH#V&0nd+a8~sr__z5+}!B#yQ z&{tSG`j8zzm9ZG+_rAX{4c~OQ8{#ZVPfsvx!8_n0wt-pmdN3|k=o@rM;LZ^CY=iH z=lb7AiRzp_oP6i7)QKbF0#EB$1euDBkc>$~z_Xr(zP`}dwj<@m}nvPoWUAHpYy(YG) zwz_B=BlgT-#u+2DnM&U52A7{RoTyg`zgJ(C|DhHgIBXIg?=^lm@sNirK2M>eOrFK# zXh8w1GnLKfS`RmrfX?oF>tq2J)+D7vnSvcmR>^Q&`^|Ot72@zE$6!~yU=*DJrb#^n z9yL8)c2~xZYhDKPCGN+Zjo2`CC4WI}H%~Yx_yW|4S;;XkpJZ|M_xDz8h27)rnPN%c z$HK##Ri7W_$@}-U1AWYZZ8yOG3)dd4HLje)&-RB9=EJEmJF@5OSIu%$Rqh}kR7|rL zhZXi@^1(Oen?`|~1yV7$nF3yM;*7PFNJl1b4IUi1^)1m?n=-ywO+`QcEGpfid!qBH zn@M4ZutSHKk>=npMPa`^_x!juoTS<5?GAJIhvSD}pPc{gIs&PJc$A5uiBujfgkx2qw%`2VH{i!0?(dqtqp=6xNW#JkC`I5yPnz)BUoA#kEd+24ol5UCfOxY z`X;Q2!TX?_z{DB%gITqaWa@>lw%hEksbXhhA9?-m-MSEbYo7r_SX`E_zYhJYLOWsA zYnO!deMq*+_Gqd>xC8p%l{zy7a@ig=Cp2^>PvY9ONa8+NIb>3o(@5hSOr!fn+nRj^ z10zkZxKO-#%&=>lx2AJxbzS^YIt<#pf`<4x=kpsmRaKG0$;>*{dPa??zl_JzXr zhwAN6dNH|C>@uMj2aygfkkSkhtX2CRPke< ziRJ-ABpyL!wZjY1dCk~#Iv9bHz$&rY=jJ%JvqUeNxrVt9N#DzU+k~Y^`y}H`PhO## z9oPEDqqSze+8g=Ly$O?K%Su5R_3HP6aBf)Qq}njo*tKz@Ytq5f*s)ovE|01rcU#yW z;lwZ^*|L{x8nxa{ILfKdusSTWUoI8?! zb(qZC9JA`2i#*?k40Y^y-yAMTs0dx*FrZbY79K>)32@Nm^VMv2eW7v{Ofb_KR?6a* zY6EE#n(1o?pE2Q}-sGL^ul0l|=5Ns_+zED&T%;3v<^MXCwL5Rq=;HYqDee<#u@L*5 z!fuU42`u7~GOIXg&B9%%uS*`hsg84T>cu{ivk{HMoAP;#w`yjBQBhT2gnZ7^F&(&Y zh?#jAIe5E`o5*j~dgW_$8^VWDRh_0Dda`8!2uW6jnuS;qSqL5?q?9TI5tgJF@|2#Adcm!=-#M|85UWJUjXw1=L zN91MDBcINZSQ#1Bym5+Y{kC2wA?k@9blyu81chOy6Dt+)d}k>f?qYlxb;dc<11C-7 zUR62_;&aK*pN_6MS(S6teUtc)>w2alI$-jDGn9mqClb zf$8s1uI?pU2dpRNDJiSbN1Q|g(`u+qn^;4!C_(Q_E7C&Bs&96k2@xoxz4+>?!j)VW zXMa35Q42i{MU>av2?SA&xM8yZAe=dx$}m*Zve@FN(&>G*(b0xxn8+;SSU6g7p<~XF zZCZDTsI4I4UROK5s&kLX5F~QJt2!4{M{Uu4HLsF8gePKU;QASTZv9K2;ZXnQXXwoi zysaTvF%j^kmq+vPL8YRrks#yL@!m|v#absfsowPV^i`jJHws@0F#!zAmWPb#r@5T- z&J1{T*1D-WNgUh83y?+R#TrWMYW3&fUT;1Pi)AAJ%jy)uIX22T5$)i|FSHz^G0N+S zgP-aP+sIPN2Xvp**2^xwEe_JuRIqpY88J4tV5XikMnEz055A=D#gzBcI4T}Q+k5L+ zsqM`d(B8oT9tBj`Ma1J9)8U_+c=^vmygq4+>wdymL$^T3Acmx11b~G?jbmgfYQ03Ad*tBx zAs{M<5Dw~ky53*{*$?0Mk$U1q*n45why`_BkLL>ho{$YNu?ej&s zrflNg`Cs-A2Zn&ug!+Fbkuf%Bn??q~*L=Q1KbzL;HinT33xnS@AN$Od5sNMlC+5pK zN$!o99~t>3JeV>WsggQxImlSa6|lLyB}CTFs3`&v4JVp4UWqcwVd%lQH$&txnN1)q z0(+Q38d)f1S|p01-&pUAa-^GAr9=%JYBmU!jNNeDgCu`ccFTHS?x{^erx6te7~?>b zpn;snR1KVB#MaO#g$ZCXN&68c)G%ql3RQ5}PLUen+wd0%jDF?n#pyadgdqr#s2CC` zJrW!3CtYSXl!}o(sR*UK%}xWD0;rQ5Ox5SjHoxWHCRL?j0$whLs-tUk54*r5(f9km z70djJqCKfx2_cvt!5)OOL}u29w$o;|O0bm)&Lj={>EkdxUv!PDau6di$JGlY5qFjb z`5K`|E8*ih`j9M6l9`!9N$kw~n zBXxKH;wj}o=wf^~EWhk|C-Vj6~BjHSa|%(8F+t`e41*nbHY4-St|pcf1X^ zfA5>_{w#Zj9g5hJ)&8c-ly+%8fWwInd-i=)VfG})#`P-@YqNUzRa z==ASitaY3i>e}QLN>KmyHMluaadiXO`EE#teDx1?pFxgBVrl5dzK6czUs}C(Kof_m z+~iphcNrk@;)b&(={^E11pR==SiDZd++(`G6b;1L0`bcBo{|K{U7uAS(2v8WuL8(+ zMHI@eWjm9AXN-@HSdy9K9r)im52btqvDjz=Ei7O1n?ihev*UaTNS_m{b9hDPQpCeV zd@-EWsye&;$aUcC1fwN(%rp#K`kf{35j^_#MyG{vT(;GgDCWf7WSZc%!&X{*zw|v# z3B=3Y-VW{;x;<^>%euE`%~=Pzu+M@42msD+;Tst3`jr;Xlq)@?HSV-nAdogW9ot-N zrB|`RS!K@l-0r&1DCFyuTf)tvsEsDW>SQF)l}p1jthDRlxaQ)JBOv#kXH?-v!r|wL zJF=B;(TLGq*Ja;PdEtJ2|Y~}vUizOZ5e{3v^36JD|7J2jSFIRt$`$!&W zHi2I=m0}TQWfN1t*{ zZg0nDQ&Ud9Nq6K@yo10w>WY;<8x@g)Gjs`m?lO$d5x@BCbk=C<%d7sa{8sLA;&t>O-IqT!z{G;G;vrR2V-jz@sWr)OyI~o-GkbCT1 zPAa(_{c?82av8_oFW{F=<|O=0d?{e4dVIdRZx%b|lx9NW@qADHbTxwf*y8cmO8z)E zb7-b^sz?91z{kU3X24Is?8AeHCo!?h6-$BLmn^BsM}w4H0ZKKwtc$Tj&koP8N3F2 zi}}z;s|8F+8O|2$E3B9~RiQ-82W!|p6syksX_EhnQvm?x$K6U<)-*_hfO@OgTmHIp75*-l&q;!1AK&Qx z{*L_a$&Hv{c9!g(f#;T!>&^G1pQ3ag@npT|u-U^EINsYsB&oKm!G>EA@|bMoS|zSz z^s60j!YTq1ams&AZA%E;L95?X@Zlu#1=HM~tUI1vr36`f1v7r~rd{g^BD~(~f491K zK5{ir31M!DAh1zplX}3df+zt&m{39Hf&9E(5yA=_o~#~-`nNgRQPn)&iGWTTCGCcl zmLItVneU%XaMDU${F)2C<$j&mb@^N~$nnFgf^%)Nh)!Mo3K~0=;e|xWAwBK$m9-T0 zQkRr0zV{Kkp-{7m7hX%;d31|s>a#j}s8@|vp+MRXwJLJVPe#$(IHG%-L+`@w1 zy#*pPorhaInH^_4m2LXN0@X=x^Ww?BpyM6UIhQ2o#Ia^lJnfiak%=WP4dLjrAvi`c zt;MN`Yd+Ev{zw(6X_aU_-dI2`l?^%KT>-XTusa z?@~S99oCB&@_&p}2Z5yNNxZ&H5bdY@gsqL-`l_ixH)1q!|1DI@_t)p+)wW6N20=A7 z-Q#!B%aB0P8J?3So6&;Y9b0fr=$mEVUJLoGQj8~#eTGHBd&>BL!L~zrM_)N}95_r; zjF><6!$n|T^x$ClLOh1A1*MctyE8o`(L%)>EU-cyR#@*9N*8C{4)q2+lfx3ux}mCS zO30aK8yU#e$8$Q8=s&^MKp-_$f}zuZf3RZ_*nrG73Y}5^GiX7if?#L$*VPGzjmQlz z!-<>}2T1q9+(SN#A#!n2xPMxWAmX5oR~DW-?mqCVpKy?oo@>b0faf>%&($pYGe6&? z(ZenGEq^fN2~|7$Tr=Hshdr*udM&n)#VLiPykzs&VL&T6nLF0=Pb=DFy^EnpNt)LC zQay>%pFw<^dpjAMzCvvs(&Y7luPNRN5wDqy46 z{7#wr_mq@@uF^f-B>}c!J3p}kA2Aee4IzX}IZfuGIC^E) z#H)}hD#KNZ=v6C9IQluk!&xr=jy0Z-7YvFn%?F@N+PLw-5ca7YX}?~7}m;l$#ILUyR&y8 z?R2afTJpK2foc+(*`{mT;Z$mjU!3&w#|hdUh!(P*Gak7vB7$IOw?^TVORZhG`oPM; zjVcjhy}03FAcPFvV}=8Ck1o@MphlIm&pkGM*k!^_^#v?nO_WC;Xt4pjjt+Z)0L|*a zjT}yxiAvqpvEy5RLh_Gce0`y~ZRo5t18$$hQ>j6EsRTfwToUjpVWwr9w)IkZqF2q! zv4)^{;A0#0M1RH?G)cs=Ic&F>su;frt2=tu6(QtU^P->AFf+V@OPKa6mo1z$82tL2 zlRGiR*rzPf=U=F}8D%)*-z11&+j;p1;s1*8s z%6nVi=eOL|v$I`b0Zhds>hf9AHQHoaRufqv5#LRJ+9jH=9CZ0jrIRa^JN{4#{l!CD zcVABvYV?sTKzm>g;rw};I-nM=YVid>^N&!k{UDb7(Z$+BS*0-A-b(e<`}N`tuj zXaVER!jY}jbxdmOtYX+0!Q%%WoLwFR_ap92F3p{d?N$R-#LeG&cXPr4zZPE>O|w;z z%{WhV8olBa>o=J|p>Ptihl0+ZhRFpXLnW z58D#}#s4iKbu8tqhYnu6`@?1Ayb%wLVrQ1og_h8jdF-G(-P7Km->#_wtB*am!>~rfPy4Lfd`>3m`u^f1O;|)hEVrN z0W=T0*Q~02yD||oSTH`2_7X^&5{h*<12}!D`Jib1)mYsw%hxR^Xv>I`cmGL0uF-S= ziC(L6-fW*lN^NoxKu2JO|+Qi8Cm zLs$9jaxS|$lGs8#iGZ|rwL$*w7oYgA?<%Cp$;qWTj+ejn&7HB%1RuOr&d%E$N@!4z zc)?$HetGu0zxflef4I=VZ(UT?MT&=qw=amMDzb=4L{ulO|5kKicQU6i2A}1tX!0-> z3N8APs1fGFE8zovSxeLAiQ|=+pp?y0pjUFmE)a}0 zBVsj%0>l*k6X*nIqUI4^ zc@ueI>Ma04CZLi)Ltl=S(duUj>d|~%h~bCU-xb<5B?$DFP5?XM65vDBOObNh5ulO^ zrr%%gOVl>2_LBME2En5d%c`2vQdIJs+hp*$(j5Kyo_q2AZYS$knHptq*Z`l7v5xx| ziLftk=hdu!_7RYSDpfC*cP2T-LBeOYYb-VD^uC-FIKh}C6ZTElsy2}OS*kMkdt^2o zJg=U=Zu29ZEyTE(h||iLZ=SAQ(-CTaL+5U}63XnmFOy!3_0*&K7Q7cH#4}Bn- z5%a&>#{WB0jNIN$kkI#4qOPesvoEuEqp$5xt8+*^F`u}K;2DU4a)w0EGf_3b$%)lc z;S&w&(ek%Xuj!Z1UVnd;|Ee{S&{_jx!xs|kd((x7B|HGPWd>GqjzRwc;3&wkbt9RF zA0VDx_Y>g%4SUJ!Z?gGqdazKb%Mtv5>VePkYp1Ts>|O1AuJ1Ww`|no*5U^9mKD|v? z-Og}p-3CVcFc#}9fP-xfq_E@MguE};B6<~AIwjLs-j@J@+Mkbi7kBwE+9a!?x~y5u zrte>AP)B*=KvH8{+g5e&>R_%RD{h3fwcqJm6W6Q=AuTSbVaxB~dfSW#2F&esN|I!$D9g*rPI%cqtB!5q!{4|2imVrQ%uvEP9`*1%95 zK$E@}d=ESDTQ)aR6zuLER1e#?2qJQD1RFSLJrH=Y-URELZ?SMeSHPuVMvvSN%X0(`mZ#AL3GT~ zjVdYX)xC0H#Y}6c2uT5HFX~v8?@!nI_eU*%TCK7j7wV~hFq)O{(b7h~EzWDjh&4Bf zb4EQ$e~rd#{Q?+MKkLoMJO!(Mim#t_2f}Qa1j+Fzf;ae=Z4}!~eBe)@;D$V4NZfnAu6k{xr$@ z8c*AO)a#FUx0adJ3YXRfj%zF#ya&E%VNTQzrbId=MYgfwM2FFEp;sp-7lu_7#tRBG z0-;qXhDRI%G>)D)DIw9_i3rH9bV{p`T(8Ln_Y>0Idrd_g6tnd`&(6z!qR}DAH|dJu zXe6Ul@abx~9F}OQO!`{dEc#>sT?xsa%Y4ugf#YPy)561FB~xa6{Giw@*?A%f<$YSz zg&_{Z-WbdG@rEZLTTj<{KFTo8`1R-TYzt=XZ-T|>Y*V(UyL7L9QLfMtCJ(PpfVePr z(j>MQfyhoPLhB<1f{qKU!g;-pNOq`d;VK+%Zpea^P4!fI=%Dx_-!oekf$2`E&9F^HdR_V?jev)M&MBNiP?Sp(l~_Mo*yo6bO})zEnxl%p`8 zW<^X3bLqlMg2f#8Xo~ibWXF=a)Ul&1Ln;p@a4Ud38y|G!_im)Sgqr@z6_Dm9Yq9IF>>xjyb?*p6T@4!l&uJk<~<_O zNJR)YDeCO7*JI=B?U%Tpcj`C2hBg7ZUJQmYIR)4g8CqZ0ioqq?@8#)66VSS{PycWQ zfK=?A&r}jKFYy|*c?q6ND`P~kXe?%JYFIP!Q=%CQ;A`FHu^aN=cDvakTTM2UBob`AaE ze2j(8ax$A(J%d~VK9kqEAc>Lwn5{@E)@`xT+Il#V92g^S$48)9!^$lwm3M%(1c9sO z&Jkdc4zld$c&wVRi?DhJBcl|-3;A4gR%dxg#}mr|4zW8aj{|Y%<&1*c@iL#qO>Ddm ziWZS#9zcS?I%eZ26K-~_U~4>n!{8jvq~9DX_vp4%3)mD|G73o-0BI37prpN->GJdW zyVSyRBJey1a4t&-d%gOF0|4$RC|T4gT53p$+vXQ-2)9n_5OLB~j@c4j<8P}-;|!9w z14e#V@1Ehv2MIS<6NK+0zqMWcKoT^Wvq!*sJvw%Rb%EVu0{Is60tcLeAH*s%enN?FnoXe0$H!gry{Uj*ge zRcKMy`XJq;bM$XzQ@%2KGd0G~XevLV98@r|(t5BxhrK~5*Qz4AusPs9aN|7&IBtH| z3zm%mFaBR-`-YA=<5=+OH~~0|gvU61EI>2P`L^|&a7xJ84M3yK@Ie_;5qaWN$`vMQ zr-&kk1|s>TXtNI7!`C^82DYl_vo-H?dwYA+lZPu0`4gNHtf@X6L;~QOj{+-Vh;1FZ z&vPMlkFBxdVCRCZ{Aaex^-)Zqj#bPh$>5w01ZLF%AfRHudfLHy#M?dnX^{{NeUKg3 z7|Bvr>SM5|DUh7xue+&T&Q*DJbTw9(c(Ue!0Kf8~5aZ3R2C&5*G(E;9Xg-4Mn$*YN zy^JC+Dcv!2dIoE3u%@{t2RPmRV^T@ip5$t|-@6Q}{6 zji?|bFpT=OM0-2>W)`fehbg;JKMF=@5kx3dlsi0@9 zEy^K{&fDsNcR8)`E>omMGFy7GvXPU6`i%%znPV|2qG-Urufid+T9hi6Ng!3)pDEtl zqh;Keq)Npj4!MN9@)%L{0J?XG zQ26x%U^P5KW$-wXx@JeR#NoR-yWMD9 zhB9F7Ru)+y(0*DCc=0QoFV`ql2cDCTlQZRRDsMcGI9wZI$^x>xSsZ;YL@AG!KlbpEV$ZQNJoQ}@0q9%KXCr{Czb z%)?-lo~FP*C^|9o|7b}8T8_4Wr~4}q&l_egE6=(UWnvEAVb;ko_qhTl!T9`n?*hg5 zGV|0_U-|RL^AHC6#F6J!8;uQ6OK@73?;Qcr3$D&uf3QV=MW$S(1ZAGA@y)?@09qWE zgaHWOulz}l;dHfdeId_NV+(TU(oTSUo!@M~G!qC7n{KmrQ1hL8FIKcSf&Jxn6W#l7 zd1*Q+LBm#J+22b27UrNRobBEwcp-rH6;lT2*X$O6N_m)!00*bGP;YJxcru}g96C;d zA0!V${%!OCRzvi!C19D90=H+?w*(v?I_sID%K`lNr;1G_I4)aI643WVIEW9BMFct# zno3SG!sVMEA}`te9}kCT@!pR?nPY;G(1<84-8b^YfO5LYY6DeN5GqlV&~w$zW-o@1l;zz@vw3iy@0$es=4&1LT^#t3hX{?oe#da4Ihsd1xpZiTbiec z;633d&kqjj0j9HQKGVF+Qj240DuZ@?(}1OQUAkoZ))#L4lQheT%mId<^#IZ$ATnl1 z_QVJb!wXXue4_K%*aXizFtsGyq%C(rFqmMPz0paIR%Btsx?eNN*Josb%l;eUfHHEv zDqW6CpahiBrU=xxr~a_ODy?WQSZ|rZWT*fUdZLts^WDkyxQ${IF^Jf!m6fiA#nbcT zK-&9HJ09Qt*2uj-nU7;JA`tA0ho@dBjT-OBU3xbAoX%$ENgmxY`pM-GKs1b;Fc^8i zuxS+6erv3vOwD`T@~;0#1&9-UZMJ~=(n*fGn!8_`^c0Dt&TuBz`BRk^H7IqbUILJ} zQ^Uex%m}IeRogM)(6?(G0QiQ0f2CbyirEm`v~c^}fJe(83CEd`Zh`1LfEtcKH0w+` zNSL1jZuA5~32x55OaL@ML>gaDmJKuubq$6dILlNEJe((brR66!$Ecg>7H6EnqC*mK zAyY6QcpxaTPBvL2_i5f79ucNm%%pGwUZR7upc12u?ZYqz%u;0hZ7=^RU^<?`LAD*sLfq*~6csbbu6)X=3DXrtVJ=S$R{wIiOs&wf0HtmO!qV!z2@kN%Lzm)jE&8c8 ziI%VSZiiLz7+Gg*f+~c)&Odmshv9>nd2qS_r%f>=l#}4c3E(Up$EU1;rZUET& zlW8F7D9R0P@EIp#CQ1LJn1^C!&(#1g0n7$GlbUnLJ|{r5m}wuxGjra86ROpn&L1XV zx!Icz5h9wpGPNL;3<6wl%TdZgjDz!37)eu8Ow8 z@b5549UwHnI*ivSB|5>K{cm7e_XtX|RZ$96U39#YURNiMo@=!32z!VIisa3`O8a!Y z2zG54WRP&n2T&^1Q;)pt&J+n5Z%`ghhAqF~dN9uU#<~E&q_3K57}nXK#z;sR#+$r# z%{%-JbK3$3YZ~RlwN$qjbBiJGfGCwQ4HZ-k0fEG;&uXTRv2VuyBU}0<6nH!dqAj*v z4ziGg2f&+$N6-ag1fG?#zO{p%17U;?ty>TjQ2BR2UO@V4qfTUfr+2ZfuV_E^&G<@* zDI=)cmRyUL}$x$f^R#*;+ab40arignE*pB3`44))Q1A7 zOQPuKl6mVqqK#*`;0XDD@=U?V+`l&85Wc%2{Q zh4~Yui#2vg%qXz+FA&$faDO5rbpVXydcrr#lZ}Cx{Vd0>=``~LO7OgN{xq3IG5kIW zRsIN?%kHF-VVhe%P*daT)4ko~u)_bG$DgbE62u1*Gx6XyctfSd^MAHQDP8q ziHgqYcm~G>R}MZ>EP-Vrl4&Jc8WEYT2i2`a85PC1ZVDuw=HL>y2a(2)j>HG?*hqY=|OfS@sG z3~dIs_3*aO?hc6hdd?v4k`Kp>u+zRxAuS4k`~-v{;CTuWA(*Yy?K88d<-Hxbl<5B2 zD%ai-6UfBW=s_N7XsvhFJr{PfPlinssI|nEQmXW55U<;^4*Jew45+> z!NI|?TnkB9BS8=hfCq^f1y@@;UOT)a(obndXtCZK=2cFE7ZM4S^@afohcRhzl=&jp z>kJ``NPyPXGg&|z|A7Uj#j&Td!t8D?c&XKTb7Y1qpwM%DH{sf!e7f# z$M(hYL5^m?LgobOO=Pb|g4|yj1*Ki8KQ63nds{t$SjV_GObHgNZ?MmJM&LduSl9h& zs+-N=T&qfN8mOaH0!W$zbTp3$eaOp}9m*l>ePQ8C@d?naZQ&h&9V-CB%Nz%Yk+`o_ zwd%8EX_AbQfgKUes-F6# z@&I9N^-t=G`dhI*AO~)o|Hao^M^*Jj?Z0$)H;8nnNJ=AJf~0hJw+ILb5-K3w-QC^N z-Jx`M_g&}vzP~Z<9pm2nmyF@zoV_`Fuf67cp64@hOmv0=Z8~qg$1}v>G^c^D^r9PT zV-IY|r$80zrAcPC(Fsi`RAj?|J9`Ua$|g+f5QBUu?4Q@hs&FNIA1%@e_7aUfbjUPx zYN=>iz!pmn&Ac}hjTonlNVJ<2SSQO?Swo9&mit)h*N@*6sfQ2ET>__i_lyl=TN&L* zR~#++$IW10Ak~_@$v+%(6{N{U%nQXyAa)(_Uj-S z{?>ilYkZagug|U}vhuTFvM4>c^K}R^eM616IO<8H*exP+L@;esU<;Oo=CZa<#MDUf zf-6bvh9`Qo<KnVPw0^iAKAbT`183nBa(!ZBMQ)Swpp=fvYr^tQ zKdpaKW0)Die=jwe8)2sSOCReS1#q%v_x~|NVYqml;-geIcR^qfn4~IY=Knvw%%RTY zkKALK%ALJ$!EeVn5`Q%HU4wM+3E*`f~^EA12*&+<0>Z*Uta$@OfZ}kXi5+-%~ z-?Eddb&y4P&XA*p&)q)^9ljhya$S+9%1sXk{roM;I!%;!+m$xQDV|saa&yWQQE&Ew ze2iS5{sv9txhVXz66trw8L9-ICEU0nXGt^kc)A8F&|;1K#JkR|BUC|e_k1MS-b`(kYk6Pe&7>3%J=`2JZ?ww@iX`0c(7CaQ?$->_L6C$gW< zohuwo&aRljdS26SQtfX}{_g6V_;_|oic`om`dpCsV7`hUPGt95C8jNo?0&>>-z)m!4I3VP42$$6m9ealOl9w7KVg| z1}|h{V5ZHsGz6MWSXmFI!YNkUiz$#W8`F{`@|?Z?pawV8_>-EkeGo=oq-Py_|5QD8 zzj5{5ViAd_rb3^2^2XRIQCWGvVQV4PjuHE+0FFHAT|k$;UM>xj>*bzDdzRYK-?dF7 z7VV2(W>>?}w<4oj6*RmLcGdf#AMo@)SzImLrD%ab!O5b3^@4!UY1zo7WRT`&Ivs!- z>?tpNkIRdNF1#{Z71-u97=AXzFS@-atT8v_3p^JXY|0e=!>&)a-*6gxFR1uTTs3*W z=Ab^g$;dFLUb}mEU7NCXnP>JWA<}AuOqssa7*+M#J47uo7P__SjIB1+ z>qi;IUyFwB=RGd9i9GhH)xBuFgKEK9mHCn44>c3trCr!4SEWd0uJd*to+R0y-4aPY z*GEK^C&-2pDj751&-Ifdqda%5S5Nys(xGS`O;kB-W4hYU{hc;m*ZkapXLpWMdRvfs zW;ni=Vuv~U6laZ|sFWISkd^wk4JCd2Li#|UsnoM+D_mo;=Y5()e$~t+8FdeJ~JzSn7H4bLgRzTdp)3#JdYFB;@VXOIe1yU}j>lwljl|77|RSYMK%?#~X4h{KN?%IW0= z6T>*ApYmUSr^j8k`m-~mx{)Yz-jaJ?` znO|XYZl2Lj0@bwZ&oKoRqbMuwh7Rz*$vits=ODocx=P#Lt?LTrdx5G6?fszy69eB^S7GOo={zcX^NBR1iy1eWL@Me;P3Xe2iX2e^w*f%Id9Vu)10H&ha!c(~5mj zJn7~4Ec8N{%NZ0SHww;4!f*znz%oZ3CiU7pm`$%#L^-^aQZVp1I7DSTgF3eG^U(X; z*UGMel%8?XZ5DHacE{qel|P(UV5cfcBhT!ks>WQfiD2KU#K?If<*3SSlrK9eSOXJp z3_jD}i%6@NVM^@*ZUVLuFBSI{?2Gwc;buEt)}xic(%MGF3UkN8ZhQS~p|R=Q>4ozI zc6GzL7P&QgX#qq&Au4$`x!e%wdiIp<1@DI2A4AsX^74jJz8^BYU6|&M)cxdYPY7~V z27MJ3&8287fGE+@A!__5ULgLZ^X<$w*IaSS;~e;gK65kE1SP0~mo=g3ut z&ss|IdShfX`Gu1;a5pz!TVdZpVH&~;ps3N!bU{gy@VOEn3a4o;geCG(^g zmTnV|p2POP$LduAkqgxlTK2Hu>k3_%U-Vn}hFUzvB*y}*irB%9;3OH}oBjee;~}}s zFm$FH5?TMkg-@kD+VTwRL7nOmWWd0dFYF(Yv#dO`?ZQ9wabr%1s9OAA(ML)S)mD;& z(LoecovI_Rn`B+uNP`HCKrdN<)~E5R#;v!34#|mAOcsK8!^h>+0eb>3hkV*KmlHhYJ5EX{UD| zCQJXQHDV&IA1|oeP+ZZ$xQ3Jb`SN(vO(95%KEblvCg*)tnqx&%*?xy_haWFD`p2a> z!}ri*Fjc1`O&@^4_8O+*mX3e_@g%AE@!r=K1h5XP@Rc~(FY#tAcaVp%96E^yrUj4Z zkyi1$sl6rierHqQv6UBwpFw?-OPqM~5v90)JFVv38!zEP_E+cMf|Zb_b4Q6@eQAVi znA4KPRS#2KO_+@v-wRe}$@CH3CxmN(Xj@*`3XF1C6)-TW3e7s1UXuu7qFCP`js#9I z`AHNQ0!X56ot$O_be}+e!Bt`WS54rTUhLDxIOFR0vZ!fV=;zNLco-^t|71>`Gjts)>MXq4*2{btjb`&ce5NwW6+ z2T1Yf17YuC?p5kmoJ$)-Kp-&?{i+7>p0)qkWzCiLI1%(sdC_z8yAaTLt=}cHe`@6R z2T*f%M^gn$wBFhSju11?D(ZTW%V-XekL>+hFz(K@wU-6qC>=ij_1})J``hI99pWtm zUD$kpi|N00DF>~gY~UVCCt%g-F&px~mWG%R7&t^qQ3BNd0!6-aJoGBKYaKXljHYvB zaW`(Wqea4}%RoTBN?b(6CZQIMh~rBsYt$J>2O&%5qlz9YFq^&hT~qUi)d2y`-;@T0 zLaw`5`9L7~o*Dbze|wl%z-b*W@xZkN(Jjk2r@I@be?IA;yKRHtMbPxG4${0_CUk0C z>|$!!BZxH5|LS7xv(Q#f7a3Uq-if|$L^9X?>98jEqrsaHMEKYW?P~MGq8)%0Dm}@S z9}PSE@eWAcLTH`+3*~x^;-HhAr`PDL8UO=7k2>xqFk9_==UimaE&?H|EHwaDW*;nqJ*IL+O~oW6l52C! z!rQkY=zeI({LX5U1_ER{d2Il$63^vB7Ru4iDr(@&0cN9I(AclO!2v|3F_8zBNDAS_ zKh|Z4CeCE>EZV=?VIxme+Ryq(zuwm>fM8qy-Fc@+zQeJ3aQj29CY#vk)4U%$1=Vyh&1d~WgqOn z15&Hbb7tX*Vx1u$^;jbTOPp9R%aiFVe&As{dn70c*-yceq5c1WbG~$>VO~&74e+_coNZiK>0Uor_(9lBaZNR?ae)shFIPcbf3Pk~LJ}Cte z^5}XqhHoo%M@B+urYL?x&nU}KKrK>0~@#FJkraMylSet;roaA2u2NglfMaLVFki|zrn@h&l`-;4K(hPOmw5LUGwgq z>FO5(is-Yeubw&6@GX3`yVU0NH}jk(a#4jI~pvUs3axIW9NMx-~2XR02v|`q{L&hjUrbU-PS4+RDRa25V+mt;JTGFHS&0sn&ZqEaZGG>Rri3 z_;^U;eKy4yF*@B9`Byb66}(pa=hkX@vPI;q08W!W6`&KR=KYKb6xE|LBt`Xmx_A4; zyJbtxmsL?wg>3UfI4;3CJU}eRs5`_JXrb?m8!;$F6u=#5g7c0e7U;*bQ*^Q-|F!)4 z#M!mLqbE86qp;`dHr67|E5sxio6q()qk5jB3jBH$_SRPwNmea(^mXV{KO=+huPr`) z=>O&*y@@|-{T&Ggpx>l3ubPax`7<=lmt)(GJ%gOwBFugo;@n-$8ALK=KF0D7kg zpWFV2{h4CIlkRRPc$M(VsP=rK( zE?$9tjjuI|ym$;bQC^ z_ti1b+R)g=4f+X=xmZ}{B{EuWVYMmy796KQiq;FjvM6-#K^IIVo=x8~1+8xkLN*8v z;R37euj~xb^-$yvgU@%pdXa~yFxRhEqB)4gNP5l>6$R1-9n- zuq#r7>vZ^xU{Fo3s0I=3elT~so!UUOJ^)MU-8+~mwxH=RX47&U!3vZW@f}0)%DB9} z#GpW{_(zm4(b@T@aL*$$(6m1vq8^LinADM$$7J}L2T-qLa=GrRaf~2}P)@T+m5kLeTEC(U$^e8en))0$St{T9wGtrMga)DKc{{$gl@~!6NIqa1d=? zNUI7k3`9T7H{8L)h8sru@~`6!bMG*E{YBdbny11Eyh2viFsIar$K>~+BSAk_k9}B3 z`&*QLi1i+l7;2lEbIup}0(woDu2sufCsd$c{{Qo`meA{pnBe4LiGMV@Db)XB2uLER zzM1q*$~lDPFPp9w84LoS{TF2&OE_N9La`tJjm)R2<_l%;<|*z&v~giF7-myk<{DMA zrCmWxKdKl(Y67eak=GHx^qwg07K>l<_77Htu=^nqfaHC@zd0GJkpz&y%%87iCPAw{ z@-ah@NxO>XUB}nt;V-nR$G0Wo5{bA;ym;*lid(48a3g#OU7`^_wK;Neawq2m$MwGf zvJfkFp3DF}$xzF+B1yDEKToP*fCnecgF^2;qWD7nfKM%L7CtkB}_p|#S zOlI^nPP@*vUpvEzQ-<=K@voLm&3SPCwt?@N<8zL3`szV>iO`)_N0TuZ>Q{di1+2rM zAC&fWf`N}ztNlHM6evXO9tX3LtDhh2@)y9>!^jf0odbTE!37xDSSG>@sRF*EtBEqu zl^jCs-wrKj2=!yb@_D2sUW1Mik5pTs%~4KVdJYViU{wTZN?1M-|Hp&qa;TYjiHb5q@K+`3b%EO3|6ZcA5(3sCHLXf??%OOoZl_5X)f7UtLNvi+Pmv+#9 z4EMsA%TDQHyhcJ@sKx*t;?`-xF2-$IDu7aW_0NTXWG(y_hUO1C8N8|%lCDBLTj~aF zeRQz;uw4O-5g8SDg*V7TD0%g`zNangueV&~I1M9z_c`URbJ$Mwj|uSSr}De>Ar-?- zg>4f^==7VE%t)8i5UgQMy{9=Y9b@FlN}~FYr}wtJ+~i#wT#Hds01dEzcd#NT!vwN+ zF@mk(NC_?2pKiMt@`>%{L?x&3+7OtHB+XDsi^6FDe{dt(8kPx>@A|uRbxRp%(F#+Y zfJV58^or?!$P%7AJz=VaB=Bzne*uija(qM|M(xW&sN+=BovH#a77PXcA>IY z458~}0!alo1Rkjpu5&E}opgTXQ;R1j*t|tl1cUu%=m0FX9P(l&a3gU-ivzTh)y4J* zr(If3Ju-|);rK3T=`^ieYz3T)tf67XSxl!$Eb-Y$LQari*j2#ne;=)?J#B(_C>@cpw{F7YZT`R(Qbg4$8?>Y zZa(+ZZx~S$ToX9oS@C|NLbySwG7gSFAglfNBw054cNSt;lFDu%5;#-#sZ}5yR@NWX zLxrH;@MR%0MhhByq(`v}C=01|%ORiaC(?yzVPLP{Klr_pciEb?q9y@)47di7$!3Y7O))PHuwO(i^QJ0bdbf(F{n*G!Z1jv*2Tf#v?!n{a{>2G_H)mE-TXJl zZ@mb@51aV$OCo->toU$Q5EWJD8%+R}Atks4j;20Ns)(+gdK{#?9`#a&8maq9wn5+L^edkFrHfB8N5>;Jt`A*S-#=h|olRKs~R1#mqq#U0yd1yEUDcne#<4svp z><(ZEI+NAN01h#zT`i&-MiTK!oDp_vBImxbK=g_L(i3L_Nmva}KPuK;)tETMu*~sD z|4$Ooi7w-X=<=4mGIo2%rAVKuS(HEI^t~&~!YzKzX%{*EW2S8XBN_CBr5DnDN6q+C zR9^Det0qF6%YDTKKR^BI!XJ3GsNt{)Gx%v+TM6HN0|k&YewUGjWvGe^roWi71a@SP z6c?Xel{PGgZ4#cIuIlS77Q@5OSSzx#h{zv33yI%|+fPKD1w{(B_&gxgCRV1WdHKVY zk}d{4#s(k#Kf32Gi?G%Hd0eSJ>T~T91y9Qxwm+52QkCIt>T{b6#>}?-twt0-i%M!l zsa9I;Y&OA9???MLton}e=HdG-RXV>$rg*>A^jU$uv`witL@6KErH?WCU&Bwqq84dt z%D>pCc0VaaZ4G4fm)j$cR~lvGd?HQxi0fx|AMoW09Y{UC+3Z~qW`|-PPXP$TB=<>V zMsg=MN(@j%7I}>r@=Jwn5p?f>BBGW+vV>qhB=KrcLpkeq0A!+z?3`DnKmK=Ei8pOo zXkxUt3R|mxC%+~o&e!&b*$%29&e(U)1zOi-eNkArGx|j*^I2qv$>AQR_pa$th6W|I zqdkyZ{={oJu*yx1$ZL%Pv4E4SuSG|VG5CEj>{gX{M#PT?xq-Fw;}2yHKoGejdzOcA z9x9S${NBGPJSRn~1NgzKk8DI%!-5lWCKNFTz5)EGyZYzy$pOC}a;ZHrup$OV0GeyHSrc<+P}QYHrUXF%R$N6NwXF=lh>q8c{m-RylYwYXVFj9kv>qj7?G+U zO*bAwkqR*EVg}*jL7l9upBREf&Eh|;F%sqY2c?I#LYdXN5UnSf5AX38|2LZpn)Y-% zx~JYfnuLKhKMDq&@`nGoY@NP~rIQgkJnvZqfRnGGA%Zvn9TS^QH2DYIeXU4aN8?$1 zrK=o*g9MFo@tLB?yf%8@G%szGcif*8E$Yg0;2oy(rVd_P`jMjs+cX#X6DlFjdEbTF zS+=R^HB2QC_bXh~;v!l;Gz8$43{JundtDqNqpa4X95W8>7m?+(`&!bE9P9nGsCRpA z4{gs~e)4j4f3ViM8$gXMbhP5#c>ZI3;5u4YIdD+Y(&MSbQX}QgcU*JnDucfV-cmiSV*+IzTf$-O#CY0>{DYe6JBuL`_OsqL};TbJVpkS&R?7Hbbc+b zy%kkyjS7pt^+@P$^Zj#E;>5>~BT!YaPJQ$F(8J*qZI6si=i)(geR=VCudOf2<-7XP zw49DZuY>_Oddk)Id?btyBE9C$8`Ibr%MIn{ua0#>WsFO&Q{^oZn+g^!Huy^$qipCg znP|r{21kbSu`}_@X3YJIg6+G{lDv*yU%Ui|pI8|k}(kxRc=&9j(HR(bC?8koOqA{<5H+D2cVbNs45=eP*Y za>Ze@l~9bwg`tu@5n*RFKKnZqhbp)iU2exqj~ym+hOiy4bgiW%IiNjS)`lp494khm zcR9J_bTzzI#>Pav;;_6P@`i~C102wBoX#llAj!aWLapc;@~+PizY^fe*8-~Ke39Ml zzVKjyaTInD(&8&t83LuOtnjaxInc?{exfrX>7wkVjbbXZBY&@e$+Yu4u~^(K>K6>F z%34V3-k)ELO+*4swn->cwuFptVIR$h9%}NkA`P=?9f3u^y-*O1k{0#IV&N8X^x%UZ3?mgnVIk+Z6jP83K?ozC-p=qCQ{K-&|>l-F@ zC9GyF^lve~^ZLg{u{@`Szwrvw=3S%PDPN4eRv^#C%fN##`q4Lp=uu^p!sdY8RN?d5 zjN!vC{lEFE6n$laC`^k5s3Uv#CBW9gMch?srjBkigkShF{(Jl&o&iX2=m^RT=Z)kC zZ}a%I2V-5Yc0a+eI_6QE&98{m`=OhCxC;K%Dyc0moc*1vZb9y29{plVXFuS|DE8{Q z)Bivqu)3s9XMWkYP*98rG9GE35R>9409*zEedGCXae7B~Z{|gm6l0>-$s0*xSGdkw zLb~1JZc7t+KU?v8$IS%VjOpMR&tV-sOb*c~PTKK&z5xbAaIhmoUYIA9`UJi9UJzfo zp^{zf&r$_57fm{Xp10cX6fS#hbJL9ofq*MLNNX@;H*n*N``I!@r~O<1Wri@<-=NWC zz%6MV(slGU5UCk@YeC_Xukt~uz(C&B%dM%W0zbG;l-=6=mzGmFqnfv`7fg&2qgwt} zb5mA9Fh^!EGK<3J0Kd(i{YF>M37VpNtFQ2=!@uLX2-%et`^tEKbbQtRwK=dbNkXqE z*qhWUE)%zuO6^9%v|5tP88suTR$OPeUQ|n>d@ygTP+`SZ)iU-bBU?Uya>V7O>U+9c zT(#dJq3xB}(SZWn(Gtntr5SIVjPU=m-s1k}F}D`ZJY4BjHktczx*=mJI9KZMv|Y}L z&-aM}-)gd#Axo>etmZ0IS<6ZE2j+l!BZdBawX9y@f|C4MN->xxUE{YFCC(Ul?3!=xZg=2sk@UD@4N$jf{$gysu+$87P-w2jTcSYYpwKbGuwu{wPG za5A=WOZM*7PSjKxW6-W<$7g}F9W;Sf=O~1`V%jR__4l8crJ8IjCdb*hcz#!OYlk-bW@s>2muatHM+E)zkVnX6D%6H zfy-Aoh>zpjtBNmVb5RTMw(Bupf_WCgfI0a#(qsX%{a!8L-p(`IUOU?Q>-iAyJndrc z(oWlCre1HxSqW_wKwQ=F{w*Pa87?KebS*y+mxgXk*C+lE(#N1kxwRWk;N@-Vc^w;I%Z;_yW zBc9R9ocGn2Z7P*>^#1aJ`|uPFtF-y03(tj`TSGM*wyXpCYJL!3TC7&gsY8}b1?GrG zy2uz0Q7-^iE<@i4?*l0wVoDG6m2p@M(cZ8zUS0xsqg6>&Y>0d1nY+g(4xvf@T2eOL zVsMBaaT{M1>-KyV%mNcQ};fYD3(AR=N}95j|#6 z>NUULyTHRM*U#HoT_d;iP1k<0reDoCANVcr(~F}bHty@#GHw!`38m|VS!S?)i*^@Y z)gPOd3Z*Lg>6Y!_Z!1F6;L1=yS1osPh*jQLsLQS0hKAxTFYMSby$u_5dhCo4@k+CE z)KlvjL87nzjobk)b2&W%12{%iH-tPQJ*w#8R0jJ)%ad#!i9FT4 zs1xM+USt?{mcR*xas9V%haI++h57zsclamn%?FlSozzou<`J#Sqx3`zP>z{m(%Cqj z+O0Ayzh<}94rdYa;(6Y8xSkArceRL_G&S>-yikmaD_^~51CJw%E=|0^@rZ7ZIS5sv z8Wi@O$7r*wXDm;-2}_T8tR!iO$m*8DEfOQB{b zFl))Dhyz!}Pd7YumS9n$facAG+KW3Rp>lEbrTzuMh`%<2pfLVe4vp#R_ChzP zS%e}%C`MGOx-d$K6dQ*?re8N?82AP8hKNLBTPe3@H+SM8gtp&ihQ2;rbVEH-&Le_I(XiM)SPm zugzrc_BxC>qdE9b9s&U{*4#=^SOj3-^V8Bhcu85?9Gy}X)-mBK#B8UZR`TC$ zxVmKA@>hBy7?olyAL|=nn4(p+@RAN$CikZr!!$Pd(XL5A>B5cb5CkaN@7a*PL3QeW zpPQ*~I?iGgEIU%_kd^EDtsldgKNFuX>hMqC#%nB2Qs2rSbJfA{Ur+Y%+b7?h@EJ`J zY&%voOo;8d5N3qnKIa_4#9|6AdbX%vt-Pp~g_k?F%)CAOgxF?WdF1ccATrxx98a3( zzjyrcI~EXMDT*94WdJj7jesfD`xstx4NX4efr!FDg<9AZ-#1gN1fiz#!Y zSI#P-HyDvOQ1bAL^up>~d@^C8YnxQfDa4X9a`Frt;UUgvXP#~JOu15@!F$ZbWFb98 zqlk&Xes2`sIL@#Xgdeiq7l+Z6uzrV1yS-9P<9g3iXYXs{q?NAAJg4`=Oy3i_N6l*% zPV+@Pf~7m{^2lEITKQuw*(hN2HZ? z!^oeOx4+jSIHpZbBbye3<~K(+TMKJk<9mMj89M8Pu4lOq*E%eErJ1Z%w0p4Ry?EUZ zqlYuGtI9S)tL+G81ZmQ4GA~8J8$CD#L}ai3-NmYP+qSm8keUV=%R^GH*b_thX+ixXi|h z7R8r!YD3PzvP<*YG>eDIVD)WVXgJRvPPU`2mf_u}D1>W?kRR1A6H}-Cu1-rRqnMG; zZ#U_-SUxgKdo_S}s?_>r3oR!>PkLaYdB~8br2`$7T(7AP^Fz!Fy30chihQ$3OC}>~ z6SpAom5!MhOzsp3L4yB?bj?esYDsE2(vOZR&?MC2NnLYXJN;3trte-pjpFk;tD=3F z|CjHaTt-avjb1f(YpGn#ezzqR)^0USWA!xYN#A}dD@#QQEmI{kjZiAvb91;gdMce` z`Iy>iXQ`{=VeC%vg?Gt0Rz^iTOBJm)Oq?h^>XL~j(|=2(M3`6ay~oh}C$wcmp)!~& zo&J*ddUuN*FcQWY!Jp<^Vn=RRdYQuye#2bYpzAL*2RL;Z<1@hEUB}aw(I_YANYG>c zHJPzSXC4;HEOixq4PCH0TPaN3#TOS{?&Tg(WkrsP7RJd)_H-m)$v>NdE&f={i#OhZ z3Wp;17J5*|$iLUPP#&HE{fN5T2$@;I+TIVBHFpX$b0)s8xj_3;#$@drXN@%J_QFVb z|MP+F`II!Lm280*n4*}l@ddT3+AR19Kc>3OH5W_%mlsb;B%`^5hpkaSuSQ7MbNF|O z0bIxyFZ6`KCK8od!MutuRl(uN5*2k=hl04QqMe3b@1%gol`A@o+oLfin;GSSnZLol zQ7M?wJdvI0gHs19e+EbIvh)0hY<$EL7g6E5s!>w>?6?2iA-h6%i9C{$w}z?UUMjti z?q$GeAV-W9ZC?3Nh`@}Ywe!VWeOSQ3wlCp<{UJwDq8D{5ALhByc3b^tIBeJebZ%y9 z&x*{N@~&eCv4G#&uNb)x;4OO6VMC_IJx;=v^Sr%OfqBH(=>Yf;@bK}ah`H$=FS4Nn z2$5c9|ERWG*lZO!_Ch;$IFfjSDI+|%Rnx&q9F2z}ciLtpv82fnvIN=fuZK^O+0q}&uL~i=jw@!h{P+}}VQQrbS zAb_!W4=f+qU__cD*;nA~*^U+Tu*WC|UO2a-MUCWG;7sc?du0H!XKyiLU?DWN&5aES z$XGZ70|T83L+H?A%A4&kYj7la?qg}fUei?55N6d#1k_huuHG2qZ@^L|4p2RYieLnl zg*7nc*enC#M+J3Z|Qaii-V&fUX9^|*ekcbs9;STbMqxH%GcjSDc| zLB`|hTtT!c;I~o(pH~PJFbMwxP)YFnU%;F@-xL8ircNeLKtw479=H^O2bA#TWZ00; z@w(Rg!0FYin}BOFZ=@!Fetm7uO%yW+P{LTmHbp(*^wnvg{ZEW+vn5@Y1N(^N{ zFH7Fws6UGqXnT3~d9wvBm%$kLk^u1g5M!Z<9iBCOF>d|ZmrVG@IB5t-uf}CZ=@Tya zA+_jZ`KWxrbfNmx|4uKgG3rJj;djoNJ^cK1KLLX5mH^`hu>@EzH*lhg;B1a0$5(XD zX3Jm-_&*E5!@;S5B(Npm!LiJbPXgw^&gDEqkakoPULzmkbk~4Xp98?&w9thV>H%Di zPPI9D{UPv^<$y70=h8$x=M_fXT2$EDApGA&HxEfq(p`d!fDw%5Nnr<}csN|r=Ii-^ zkh;DcPQwl;f=`xDz_@@-p)GbgSE`#2JlRlJ8^6go4)YS2BMIL00%zt3-5WTxMRn!z zjNe^nS}1w0z|z9cuW$_PVA!+WfSfiL{_Wd0LnA3+?=BKjFRH`v<#gD2Jav-A;67{^ z3$|g)v|7iU-q&T|XyP(j&(zQqEQA&(V|dj`bU;0srhe>S##4hD@ha`L1O#D1d@}pb zi6BiwqMMLaCswE4-p)zwn@1;8(2^4ri4Jnj5_+E3g=~QIbjeyA>wv9z{FA@9?o^ zrW&+S{@O0s?Hh39H7Ju-+85@`FKv4kN0V! z1Ca}(3!iPBdy`SJ(09`1wE7QHxTj6(Y^no+$}3%jr%juK{>{`xKWh+oRHdZy2X z3o`YA1A;b6;RLJ2v7PN4Ub$C4rOqZU4;h2gFf(e1tA8lhE$gHRd)4;rf921BTa>bg zlP5Jivs=te@1N*2=))G-#$cLh1ECO0g!JxClet(iSKkR&9E^!r5f5nVy9;-@_}A?D zf8S=o>d#ljPUXFiOS*`&T!|0Fw96JYz|7?%cE#quG8996C}YY9N%{RAb-2 z!kY8aCNhc|H{!SxkywD2?w&IfK+l_=6WLR}r`v}t#H~85;tgNe zJK4JPL7x5FK-j^}&z;{*1(R=AmcG6?SfJ;2_t!fKIB`Yh>8-Z@79S(ex3>vG zHoqYCCyT7y3m#z=YWsyp{U&pB8*g^X1ID1_XXSM-Z_SoGquON5Lnap~<*Qw9F60b2 zUA!!2Qu!|hr!p;87R=ERE;QvV-S1@}rCW_m3txbD$xK3}$qhk1Ph1r7lAln@^Fl?;8b|NarjC-hexUH}|CxX{u#zV-hxf- zH%u?WT(<5! z6p0*SE;~~4U&VP-{!DT=XDeR(z-Rh3##uk~hXY7P2)=r}`=(eCSY8L_N+hXI$MnZG z%lQ?Zb$(!LvaGhpqoWHX$Njr?#X;fG`o@gd%VKc!#jM#Ei#1l<#UwQu)y7Vht-!Dq zWaifDYieXP_UojDu2Gu1qcfEr%hmaPr;qs~b2CsbicH|ndhW%~UZ(EovsF9N z!u3k%alz4xEi8lKqi2E1a*2HIse`$z9&X28X2y*1D5@TA5NZ;aG^-K_zIE z=f=-tRY%|=ls z`F{CMLs3j$Qy*`v=Sgbe206O)kQ#JIH3D{M)e?-^c=BqCTkSvEtjmS2*z3Yg=^c9~ z=XZNABz0&fQ<)McNYW&>>jgS~)n?Y+u6B2&DX--c=GC*>HKowDIC7?U;VaRMmN8h6 z*QgZ;(;8u*CL6FR)>DpDPcTjss46lDf%HkaH!F&je7F#&T`ZL;^k?|GokWpq)fT4m=8!;PU8d z-pd4gd5!HS=^BeK{fNC=E^U<^Nd7vT3n!47;^HhK6HGjUj4RUj7@ili^joX9QP&6e zJLV|M$I{Hy0Y$_?XV5xLI(>Ud=cC7IpWr#8u1w(6okhJMg)>TYQXXXtAO`vp#U7R)o!F8yhn+(EpCw2h4XQ7vD& z<~IQ&aG35tNjr%XjCWD~eN|$82qULa%fHEB3ccU)Xt@~=)BsO-1x%P$gAnuHy~`}?xFKJ%Gug&2Tdm8iq}o5;dT%XVDT+7LzD;4-Q=349nA`ch& zty6@1WK1vlP!h~@(tuIjBI(CfqTnLb?T(3T8AY&Ea@NxhC04hV8k){PTmmbRndIGfT{7n$X_wVqRfUXDxl>u@Ua-y{b1Z!pLzJLWvR;{d zGO%*MeP%DFcQn(?b^=0)a@>D)&J)W`WmtA57T+fKC?Db zDOy57Fwgft?bGIEn%l_!ywCEoc{96im-_bCoMvMhS-yfjA+f%^meKg)wkcz(LU&#j zzx3S2dvF3^R}%rvk5^2lpU4qpK>jX0;5K?VQM3?FPqmltolyi=!p1F(wC67C0u9s3 zXQ-!}CDSI4s}|3exWrFSzR*FBuf&}yCH9(>|1Ju-anvQSswDeXggGmX% z7jkB7hY`VnRFlRd#EIRo-P-q|#0!&YvW8g-h0LD4GOr#^IcJQ0jC_E3h+|O|((L-y zAB<0zZ0#VBxcX>`$~P=jI%1PqI}%L1SBhMcTk~dUPD%(Jz{!g{g&=|TEriH?MmF0Kw6uuLax~tdcSEEa4vA6$Yshl) zri-Cigd_xPMA5mYi?XHveZjGO9Z|KPZS*!{6}UfClK=0u_;qW3N{6+^n_lQ-G8K|V zI-kR6U%cn97>FsEQ-o{_>5RncfK-}nJyR4H!K$J`kAR4lp9#ow@}E9^0@&=savl-i z+aFn3Sqvu9g73VnjU-`U*s-2VRgY-R<7ePQ&Cd~o-T8-c{BY` zg4D-=ElW*FncncL_|_Cux;&S_e{2OjiEwK(xMkQ;w>dcgKcudrf~DW!7!a&;&%WUa z#x$c~V9fd^O_Twg;aLtYG4X&Gl_Qn*XbkA!%8Do-?I;M8gVEF2sX;{3^!K`jRf!pKbV0IU!_f=P2g<$!>-ONRk^y_!yjY_Y+x6NXhO zl|}>+jeeY?7F28lDZwfduxm)-4YcYhuW|QwfR{7}8JBJxl=j-$VhIQ!Tm`C3dP@Z) z4e+ko-^_3!=?k3;P)gfcLCmor7bbIaypj#-?Fqo*FjIpN6D>hzr6d)A>!ezJZt}o@ zYMZmS`~U+MDuya?A;ADn#&0l%pF2K`htGaF15lBww~qkfQ<-|a2Q)x(AbAjaY=wy3 z{s36S`Eb!z&|8pjmy+!PofV$jBEo$L!(2O+tScg19mSKfM6tECmCWVlBNF!JLG=Y(l|sNr;Q?Wwz^D)Q;P9-~1aoz!XOD6b!oYH+R zcf|l^E{|N${g0&x2p=ZZ*)DYLcFHzmslyo83uca%@`%J&&_5kV>@$1s2_d*y%ni-vHNYbEvf*aj@#z34z43{a| zT)~exf*438@rTxv5^?d)<@BFeT7@Y;K$GW!C_wL@CorBDr-rA7DGjOvE|g$4CPe{+ zEoEwL=FGW2^<~>J!in%rQc?bfo5%XU$~y08xWaCY&*JqU&%dQTAIzVqFC*Sc$6e_Gl&XWp})XYc)cOxlgQ{}6r5 z2RwSg`&PqZ0o&h_xSKwba*OigoP#lceky>&vYNaA8x8FS1KugzniNyJfI*e>@9IQb z5@U#5Zk2d#O>l`toJD-1=ys9(@)!K9niiooE2t%Jwb#~@C6wcMKGe~wyv{0J!M7%p zoAo*2h9viU@NYfY7sp|u!9M1D%z&wc7-Sa8i-1+;gh$I~5Eo$wLU;8$1O3eZZj8Ns z%i_2t-g=tFuzBft@>&Mu!7@Db;m0hZ1p}^l4+Zu{n~XN_xYT4^FQw zTrs_~IB7(6CPWlONp@oO&*ceCA#K3y|hExPcZs1jiBs?uuAC^g{=HLF=gs~ z1I!8EA4v@Sikx$X_w4!sEOIu8RaLs?Jr-wem6)f{v zth@Q3zo2_^Ag+@|k{OXCT@=qDIUXUPE5-?hCP^2*!1L|HHWr`?TqE@VFVct0sSM2p z3NT_KWdYbnj#;xy8$}wq%Bb%$?aUE=0c%}9sLsxBO`ls5v5l&r`lgrODw+)bUS$t& z6f8Xc5%pyBwfA|eMoFlb|2k7^uon+?KnG8eYUb;&&#UTXyiVqrt&rvp zmA$SH$?kIDQ0wpXx_$;?RYv?~NVk$EZC@A>e*Liv*f$*t89wD3{xGA9ikhrUb9oKb zlAF9D>RbQ&XNGERFrp7HKjSpzTNk3vtuDi=*YFjgIc)+w32#Cxj-~9Oij>Woi3cz38_u?B*SVd1 zm5B7XU+6*&1cA6MQcWb&LVxSH=g#|7AZr@v;N4x6;m z5B55BRr~Qq#N769(z$s4RWrMfdwx$ZDngmFLN-1XN>4Z(p{spQB!k+e zw38=dvjXd~#Db^t3{K0yxM2jN_xro9Ap>f!#degffQOS1b8y`H(CoL=>3phf`KM$^ zwvliRTl?I)%ROf@K6akB;H=9G|0IWw7@$gJ}cwN8UWoY-VOVU@*JVQB0sw(xc2%`Eri z{b0@rM=5`zHn-Q?33*&%TZ#{nvLl;c?sYU2a`;uGZ4IG`YWgCSn|vB(_g=<#Y@J37 zc~{A33vF(z~t6DwH7gSkCM`nKJ|h$7)H%u9%+%1h+Dnw;{pg>htAdO*0uX6B}Y7;#I{%Av_ViNjtDiUGwzpU+lRtl}NY zqut+$dXe~WE@Nf{YS(8DTf%T!U&uhsVbuSk-9I5TJbmchK{wm5| zVp$%#{}Q12J%Tk}H7)c|RUSJ1?^giPJOycw+ux}I#eN39vw^nM3VrH*<4moC$r@Y> z&uq|3A3zDz1gFXlTB}Xq!50>W8DtF4f54zUA<>ol3-7&QbMwjM2lwv{&4@_j+>xVN zOOmEETKbM_PB&9EKHyK{f*|d=!UZpgZY7dSEyvyJz%wbw@_D<2Rwjw~ye=VV@H8uO zyK@(#{F(xBWi91GnVys_+Y1m`;vS|g0ZTTG+PL-D#D)&F@_CogORpQ(Dwk9TsZ+zY z!Qs{~i@11dWWzvgwy#(LP7&4Jexf?@T)!p=y_fHATB>F{<`DJm@$w^JK{(+^Ip;?& zcKL_yn@Or~&IrBh9doaNy67BEH)0i+J~CTsNDm`-DiA0Pu>Z_W z&2ZGcMu+TNdgW6w?_>1=kIR z@#E8|P}Y}PR+>y)c@MICDIDgk4OXJ*)HMQyMws5HrdL!CaPqu4CHiqhIC_D2ZN(54%i9+^h2xVPsgy}4v4SG41N7WO->)Mme!X9B`k?8~ zstX)XN!{-si42)kjmb)B)pI_(M&NEJ+I@IO(FdCRlzokJnK_|wiZ*@fcW~Gva400O zbnzE!xmbbfTnwyIj#w|~=R3MtUY#G+0|le#8b@J||14**CvHOpZ13mE-SsddGwlq_ z1b7X~j?l;Xx1WntcSE#TZ`Mo}Q+TxRNJ*KF*aeeX1znxj(A@m`#)0XKqU~do@V5Xo znU*iJp&5`S&@N8z{{A_b1hTL`u=$pdS*zk0&ORy+<6nSvRe5>&(@!*G-^foX*9-Z- z^Q5{1Cn0uEu_ao8!Lv2sk6_c$mggxIeWGL%e&7`4HYpj|#_z06xmL&7%3DaXt#PrS z+~22q%a5i!cV+~F&gT7|Kj#GA1=|2>A={9zZzkp!f*l`T?e)VTMfzsu0}4pUGVr~| zUZ6#+S=IHg)dxfBpUkib9-6ItF(y`|mCL~VuNbJ7ER4t@>+(PQzzev;#(BU;c)6A2 zbAE_6&gmrS!=yFzbX?F6t}&dUqi>p)gBMOOHs3nVg2wQ@7{)h~M6uneTp># zEMaWhN_qO9;42AjU|VPajKySSLltfk=%z(Y6MjV=77Xd-z@39jRHE_#<(+3 zVsK$W`;^=Uu%91P@Sn;Z|E_3Sy4Ue%DPYn3kjB{*bfjlN(WUi#a13K~qLQg3x}gC` zdWZ64RE1`rj&c_eUT{jsgnus;C!lMON>>?Pe+AhR7F>yImm#Ti_N@yVG{z%}S|09YG) z3Wn?*6^YLrqUnV4BEaoQsix8h4GqQMbTR=f=y^pp_hP+_j7)JjKDkF8@JKvEFiQLS z;#Xm!28gv-zapmLx0F+9xfAjQC1T2Qesa)tbKFwRfU*G03Q z6DR}R68<~m(I^cV&%z`~tfBl@kvt`Tk9q!l+R!2`L+U6MV125-1sf67?gA9O`vUUW z7p!b%4fh<`{Ao{ncUm4~&N>&DC^w;is=Qt@%#vb0 z963-7w!foh(^faQ*`%1F;)U3fM6y0wgm7T(`~aMuK2?r-9t)02-=kHLRV1#g_3?NQ zQpbL?$B9d%z6~fDkWF<8uxtg$c;sbmOtUQr;)`$#v@ac}xYQPR>um!5HczhnEe6&&x-)dg*<8BduDJrKA;k!5WD#)#Q|^G$}lvIKu+ zyYT>A%iK*Q0eM$a44?B|bfJ+YpuDoyEER%1`efnnM$t&M&*FJhmIVT3;4Tzl4zjG{ zj)3g81z)$$Gy~H8APh&5!1E1~TK3KwI844xtSXjkUT`Pgq8n5Kt{f*{&tL01@EqoNr^ZSFShJhL_C|{<C{yKOs4MzC9}y5fKBaOn-^u7Cxl1;qY%m6vZGd|~^HlSQCGfygrsxr#+;DnFFmUaIHt z3&&&WS=%CExw)+;JAO3}TOKXtQj8G5G=rFc`FDp4A1D~Tsv*^)uZ1x81+8d3g}t=O zRLd%Y(G!AoJHUN?;Pw+uLndKKU<)u_lj&UlxdSSu-=%2%T#^6#xza%cYdAFxO6A97 zT*Uk^st^I(z@I-6R=D%&!iM~2IUZ63@9`ZwX{spvzunhkxF;Q7EurNr z0T~ycQ9WOr0B>4n0pkchGI#`bq?rOXG0Dw_{s$wrVP^jb7mb^w2}2X$ z;#qedJT(Th>GOA)U1({!(|s*+gXYeNLiVkMSBdCFtGzVkO!M1jC~RwCQnmE_a1cyq z>mT?#Y)fN7xp+rUMikdu$hq}6m|AZtm-Y_x42o-!h2B{)k(Y~!0)7D)J#@M9i+F)J zfujIA{vePSi5;v0hq(E(x;!6A*Bs%--WOR+f;~@x z5LtRrxtzRIipbS`oT;Fm@0Es%58A|O3#9v7>WGo-_w&6SAEYUukQMlh*La)j= zzA>I7J9-Y1A9yR9|7T)z?k(-V9yffXBU_TpiU;qH>lJxIee!YgM_1jIY4)^9ibsO? znXaf?c<@OaJ$9mqx$U@W2x&xv@suKel>8mU{jyzl|EF0!WIcC6>+RHr1kNp1ELPux zZaHQw-g<$;B-K~SeZ96?S*wWo2n!J$#?d2~B|+QioiI!GstP!P@HRzsL0n*%B*F&^ z?F5ESA2NOcL!Q#fkStcW)+FSLkbqkw9iftCy2vl6N-0azPsGHy_1y7K*v>#o#vR`w z<;NB!+|ncjRE$54#A1pr(!MZ3bklYg| zTpp#{SOJ&*M~y>&17YjVVh{{NQR(0#$i@XV?yy34Rb;0jvQTd3W>TyGjpP6i;F8ld z^;}$S_4O?eH9f{9T5?0M-yyYiIww*}iy>^>`~~a1VCJ+m-vW&iI|0HvhJ+(f`mVuO zjG#04^ClE9wfx+**WoO}!(ojBk8~s;%i(HDtIh?cmM@`E#KrI{*BT9kHzNMwd)($c z_CG%nXx#aS&J6zkXqq2{&GM1_fY*(-=9jSy= zI-jZZw{YW=2#sgnv&%;W8S<)38hbfRhNL$7ihK|bxtB}%Hk`2S)$&HERSIGDvo~K@ z{;U{BLV1Yi>7tntbx?Dow=zLI*M~l&~8%pJQ)n61ATHlVJ7V3Ov8=wE}eYdf?4Ko=j0UFzCmf0(TZ%Go}wb>xntc-LpJ4X2J+d*931y5R8msfa{DDiW8(6x;;>F4OCPS<8?|R$@9Y6(>UaHu!aSWHMBLzW>$6^CRV6W(r zkaH7=6C602^x$L56PO?LJw!)GiBp^7N5pb;6Xjq0{Frs5Hj?)G?aGR!iv?(_HyCQM zA$&_15B$}4?|bfwWLgv}F|PE)NF8R1mHh2V=Hlc;-q!pno@^VCOmj5^a6Vk8b+sF3 zWsW;c!sU^Xo{<+_-eUa;p-EWE_COtG33H@%Qx)P8)7aA=%DEHRVYs7(n2MC-EF1?S z2J^KEETp}wAB~eo+orpUy#>DVXCUnKycl;*4is;^+IdoOSy;1=o1McD##k2ip&ZJk zlwo+j9O}q=AfIHkHSQ3z5<}#>aX1O3WCLt(PWP@?V}%G(2v?IffU3**J|&V2E^??( z>f2ZQi&h8-LA6AK>vCQMM4sG;y;-Kmn}XpM0Xv6s+<0xbM$|{nt7Isg6@g1HPwgL|3z{UIR~p_9ES*@#`d3uvf?6yq+^@%ZP`fN zcm1(n<&OW;|+Eh1=J^eC|$KIxXCSwcI-tY8R7l zYqdY@%{9^O74htDWMUmb=xi-}Zg6hQgO7Q;YO&rz^#TxLPN*J;p>4#22AI6@8cW?v zMjsO-;YvfLpjpNgZr?Zy_Ke=Dhdry}F=DFLr{ZG9Cvou5zS?*0xoN74oItOAlf`dN z=NuC#N^z(r#QSU#c&j%D@`7|mRae&ktm2=9dmse&+a5FQo#gYS8i{IhjER2=5{G% zjT6w$7eI^0b;Y0EMV3x@E2^*pyGUuCbLp+uM;wtqI z4D4{;If7flQ%q_-20NcpQTXIu-LuP(D{p9s-Q-E17!qy1sByr(vj`s4N#Wr38cMAE zIYI+=Yu^iM7^0?N_D+Fn6OJ!)cB*U{{H6xKKYr-Gs<<>12~)T)ADu2_As8FuooXwW z&(;mXoMt90@4+1JU+&h8o!??aQmuy}+}^pSq;Fje@w^S}Nr8%uRx5PcePw85{xVtS zxar>Q7R_a((3@ImRuSP_nU(dJ_M>v=))ky&o(}1olU?$0lKc04qxXZYw|4Z(-H!zB zsv{c&a2RgXl;&&)y97`iCyka^N%BOyE=DwZC0gr3fwG7_=SMy>N4_L&J4PP)q_w6e zlG6>Ia7+8XH}lixO|h}R-}lB>F5B1Yy9FYLpZr52xt?RyaC>bqdL20=21;PcP=N8NU?H@r@0>DU{r^KRF8x7^SqUiCObm`qlIQ7fNv;BK0_ zd-s_HBvfoyuxW=p@nylP>=nj-{3oR+0oK#gL5sr7$6mje3V(GB9OOrvnZpI}m0GZ# z5X*IW3mn=0E~TEYz_ihW`uj=hix+r0&VTs9k)410mfg7^HWE_xU?2mr0)6E0;+FYz zYL$)TM~!ErVB6#3r0&@Gwi#GQp|)2+(kl=(DFlW;JCLzI&{?x3ZGW=G-{V;1i{}u( za*O+_yh3S#WN?J5TV3xbGx*&M^PqRpJ{tWh7yTmcRZBq)+ag8S;GznHJjqw5y$U`{ zH|SSv!3q*>9F=`RFVHvl%rdBHN!lUwni{SMy3~woSgu6*a{)Iqa!Iw zQUz*vGi;H$jsS6DQd8bgG_DbgG-gLAg5U_oN&m=sUi7X>BR?_2L~rZ|K$^TPpO?tF zyK)pki2ZK{u=h3tA@`U-6!NEO$}pm3d&`JV{S^5~-~?`}JqKZ??MTSK3^1$_pzILw zpM@77DZX=Xu0)oCz<3gf@71|`*aBz5xIb_-EUTqaRjT+*u4=+B&`(+bybo)Db`>Fq zr}-%G+-+v&&?hWC!V(Mqi9#;BR}N>+e7se)%V*Dc;57ZIToE zEl3_1{1ETt$fw8b?2Ib{*RLF1kVeh~m#r{qveS_{H^TlxZcM{4O;L~s46&L34kmVp zJVZwx*^7+Ie>{2nqRnfoCjbIhF@Y56*V$>B-op{$rc>G<1k+8YP58=OzNvTy^sWzt zR)S6|qdc5RDQSx<`u63a&FMI&I!^Xb964>6EX-%Zq~T@%JY{v)48gbp%v`ND*v~n5 zswz2IJEX$(qn`}e8w1bbp$?B#1LxEK(Ho(9J zT6-@^j*C+c^f!KDVSWohi>t-b2&}4TQ}~i|iyU_Cv7uHz8dhz5edv_Odu)n%*A!&l z{x<4QX%dpK0P16OwD-N4zHaL9?^1vg{#9zQQnb1ekgt2(y61QYBdI#yg=z|G;Em68 z-%()b3(r5Z`mnJM4)xt+DR+%`kbZm6zRQ5mNCeV{7``0|`qw0=p$L zlC_CWH&I0?#KjTiR8io(p5ksqYg6GEZ<)S>_U9-3D5DYLQlSw9|KdW>%0BiJfdyAx zb#LJMV*lD4Vx_!g*na^bK Date: Thu, 13 Apr 2023 16:50:05 +0900 Subject: [PATCH 157/485] feat: add missing ValidationInterface::getValidated() --- system/Validation/Validation.php | 2 +- system/Validation/ValidationInterface.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 71be2b698a55..8afc6f5ba819 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -212,7 +212,7 @@ public function check($value, $rule, array $errors = []): bool } /** - * Returns actually validated data. + * Returns the actual validated data. */ public function getValidated(): array { diff --git a/system/Validation/ValidationInterface.php b/system/Validation/ValidationInterface.php index 8c018b9a0dc2..2c30d645ee8a 100644 --- a/system/Validation/ValidationInterface.php +++ b/system/Validation/ValidationInterface.php @@ -148,4 +148,9 @@ public function listErrors(string $template = 'list'): string; * Displays a single error in formatted HTML as defined in the $template view. */ public function showError(string $field, string $template = 'single'): string; + + /** + * Returns the actual validated data. + */ + public function getValidated(): array; } From fd5b43005556d9cce88245ba383daf602e020c7f Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 17:07:08 +0900 Subject: [PATCH 158/485] docs: add changelog and upgrade --- user_guide_src/source/changelogs/v4.4.0.rst | 4 ++++ user_guide_src/source/installation/upgrade_440.rst | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index df5b2c6da321..490c581c4900 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -29,6 +29,8 @@ or more was specified. See :ref:`upgrade-440-uri-setsegment`. The next segment (``+1``) of the current last segment can be set as before. +.. _v440-interface-changes: + Interface Changes ================= @@ -36,6 +38,8 @@ Interface Changes or implemented these interfaces, all these changes are backward compatible and require no intervention. +- **Validation:** Added the ``getValidated()`` method in ``ValidationInterface``. + Method Signature Changes ======================== diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 4d12d963b132..5e7ab6856ce9 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -60,6 +60,12 @@ by defining your own exception handler. See :ref:`custom-exception-handlers` for the detail. +Interface Changes +================= + +Some interface changes have been made. Classes that implement them should update +their APIs to reflect the changes. See :ref:`v440-interface-changes` for details. + Mandatory File Changes ********************** From 9575724bf4ca190ee8c482b4b86a0ed2ffc3fd03 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 3 Apr 2023 18:35:38 +0900 Subject: [PATCH 159/485] refactor: extract method --- system/Router/AutoRouterImproved.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 000afda126bd..5eca3c5a25a2 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -104,6 +104,14 @@ public function __construct( $this->method = $this->defaultMethod; } + private function createSegments(string $uri) + { + $segments = explode('/', $uri); + $segments = array_filter($segments, static fn ($segment) => $segment !== ''); + // numerically reindex the array, removing gaps + return array_values($segments); + } + /** * Finds controller, method and params from the URI. * @@ -111,7 +119,7 @@ public function __construct( */ public function getRoute(string $uri): array { - $segments = explode('/', $uri); + $segments = $this->createSegments($uri); // Check for Module Routes. if ( @@ -275,10 +283,6 @@ private function checkRemap(): void */ private function scanControllers(array $segments): array { - $segments = array_filter($segments, static fn ($segment) => $segment !== ''); - // numerically reindex the array, removing gaps - $segments = array_values($segments); - // Loop through our segments and return as soon as a controller // is found or when such a directory doesn't exist $c = count($segments); From ddc6e99fcf8d116a890be6b950f5326321933d4d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 14:02:47 +0900 Subject: [PATCH 160/485] feat: fallback to default controller --- system/Router/AutoRouterImproved.php | 267 ++++++++++++++------------- 1 file changed, 142 insertions(+), 125 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 5eca3c5a25a2..56c524bef03d 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -34,11 +34,6 @@ final class AutoRouterImproved implements AutoRouterInterface */ private ?string $directory = null; - /** - * Sub-namespace that contains the requested controller class. - */ - private ?string $subNamespace = null; - /** * The name of the controller class. */ @@ -112,6 +107,89 @@ private function createSegments(string $uri) return array_values($segments); } + /** + * Search for the first controller corresponding to the URI segment. + * + * If there is a controller corresponding to the first segment, the search + * ends there. The remaining segments are parameters to the controller. + * + * @param array $segments URI segments + * + * @return bool true if a controller class is found. + */ + private function searchFirstController(array $segments): bool + { + $controller = '\\' . trim($this->namespace, '\\'); + + while ($segments !== []) { + $segment = array_shift($segments); + $class = $this->translateURIDashes(ucfirst($segment)); + + // as soon as we encounter any segment that is not PSR-4 compliant, stop searching + if (! $this->isValidSegment($class)) { + return false; + } + + $controller .= '\\' . $class; + + if (class_exists($controller)) { + $this->controller = $controller; + // The first item may be a method name. + $this->params = $segments; + + return true; + } + } + + return false; + } + + /** + * Search for the last default controller corresponding to the URI segments. + * + * @param array $segments URI segments + * + * @return bool true if a controller class is found. + */ + private function searchLastDefaultController(array $segments): bool + { + $params = []; + + while ($segments !== []) { + $namespaces = array_map( + fn ($segment) => $this->translateURIDashes(ucfirst($segment)), + $segments + ); + + $controller = '\\' . trim($this->namespace, '\\') + . '\\' . implode('\\', $namespaces) + . '\\' . $this->defaultController; + + if (class_exists($controller)) { + $this->controller = $controller; + $this->params = $params; + + return true; + } + + // Prepend the last element in $segments to the beginning of $params. + array_unshift($params, array_pop($segments)); + } + + // Check for the default controller in Controllers directory. + $controller = '\\' . trim($this->namespace, '\\') + . '\\' . $this->defaultController; + + if (class_exists($controller)) { + $this->controller = $controller; + $this->params = $params; + + return true; + } + + return false; + } + /** * Finds controller, method and params from the URI. * @@ -130,40 +208,39 @@ public function getRoute(string $uri): array $this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\') . '\\'; } - // WARNING: Directories get shifted out of the segments array. - $nonDirSegments = $this->scanControllers($segments); - - $controllerSegment = ''; - $baseControllerName = $this->defaultController; + if ($this->searchFirstController($segments)) { + // Controller is found. + $baseControllerName = class_basename($this->controller); - // If we don't have any segments left - use the default controller; - // If not empty, then the first segment should be the controller - if (! empty($nonDirSegments)) { - $controllerSegment = array_shift($nonDirSegments); - - $baseControllerName = $this->translateURIDashes(ucfirst($controllerSegment)); + // Prevent access to default controller path + if ( + strtolower($baseControllerName) === strtolower($this->defaultController) + ) { + throw new PageNotFoundException( + 'Cannot access the default controller "' . $baseControllerName . '" with the controller name URI path.' + ); + } + } elseif ($this->searchLastDefaultController($segments)) { + // The default Controller is found. + $baseControllerName = class_basename($this->controller); + } else { + // No Controller is found. + throw new PageNotFoundException('No controller is found for: ' . $uri); } - if (! $this->isValidSegment($baseControllerName)) { - throw new PageNotFoundException($baseControllerName . ' is not a valid controller name'); - } + $params = $this->params; - // Prevent access to default controller path - if ( - strtolower($baseControllerName) === strtolower($this->defaultController) - && strtolower($controllerSegment) === strtolower($this->defaultController) - ) { - throw new PageNotFoundException( - 'Cannot access the default controller "' . $baseControllerName . '" with the controller name URI path.' - ); - } + $methodParam = array_shift($params); - // Use the method name if it exists. - if (! empty($nonDirSegments)) { - $methodSegment = $this->translateURIDashes(array_shift($nonDirSegments)); + $method = ''; + if ($methodParam !== null) { + $method = $this->httpVerb . ucfirst($this->translateURIDashes($methodParam)); + } - // Prefix HTTP verb - $this->method = $this->httpVerb . ucfirst($methodSegment); + if ($methodParam !== null && method_exists($this->controller, $method)) { + // Method is found. + $this->method = $method; + $this->params = $params; // Prevent access to default method path if (strtolower($this->method) === strtolower($this->defaultMethod)) { @@ -171,22 +248,16 @@ public function getRoute(string $uri): array 'Cannot access the default method "' . $this->method . '" with the method name URI path.' ); } + } else { + if (method_exists($this->controller, $this->defaultMethod)) { + // The default method is found. + $this->method = $this->defaultMethod; + } else { + // No method is found. + throw PageNotFoundException::forControllerNotFound($this->controller, $method); + } } - if (! empty($nonDirSegments)) { - $this->params = $nonDirSegments; - } - - // Ensure the controller stores the fully-qualified class name - $this->controller = '\\' . ltrim( - str_replace( - '/', - '\\', - $this->namespace . $this->subNamespace . $baseControllerName - ), - '\\' - ); - // Ensure the controller is not defined in routes. $this->protectDefinedRoutes(); @@ -197,25 +268,35 @@ public function getRoute(string $uri): array try { $this->checkParameters($uri); } catch (MethodNotFoundException $e) { - // Fallback to the default method - if (! isset($methodSegment)) { - throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); - } - - array_unshift($this->params, $methodSegment); - $method = $this->method; - $this->method = $this->defaultMethod; - - try { - $this->checkParameters($uri); - } catch (MethodNotFoundException $e) { - throw PageNotFoundException::forControllerNotFound($this->controller, $method); - } + throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); } + $this->setDirectory(); + return [$this->directory, $this->controller, $this->method, $this->params]; } + /** + * Get the directory path from the controller and set it to the property. + * + * @return void + */ + private function setDirectory() + { + $segments = explode('\\', trim($this->controller, '\\')); + + // Remove short classname. + array_pop($segments); + + $namespaces = implode('\\', $segments); + + $dir = substr($namespaces, strlen($this->namespace)); + + if ($dir !== '') { + $this->directory = substr($namespaces, strlen($this->namespace)) . '/'; + } + } + private function protectDefinedRoutes(): void { $controller = strtolower($this->controller); @@ -274,46 +355,6 @@ private function checkRemap(): void } } - /** - * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments - * - * @param array $segments URI segments - * - * @return array returns an array of remaining uri segments that don't map onto a directory - */ - private function scanControllers(array $segments): array - { - // Loop through our segments and return as soon as a controller - // is found or when such a directory doesn't exist - $c = count($segments); - - while ($c-- > 0) { - $segmentConvert = $this->translateURIDashes(ucfirst($segments[0])); - - // as soon as we encounter any segment that is not PSR-4 compliant, stop searching - if (! $this->isValidSegment($segmentConvert)) { - return $segments; - } - - $test = $this->namespace . $this->subNamespace . $segmentConvert; - - // as long as each segment is *not* a controller file, add it to $this->subNamespace - if (! class_exists($test)) { - $this->setSubNamespace($segmentConvert, true, false); - array_shift($segments); - - $this->directory .= $this->directory . $segmentConvert . '/'; - - continue; - } - - return $segments; - } - - // This means that all segments were actually directories - return $segments; - } - /** * Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment * @@ -324,30 +365,6 @@ private function isValidSegment(string $segment): bool return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment); } - /** - * Sets the sub-namespace that the controller is in. - * - * @param bool $validate if true, checks to make sure $dir consists of only PSR4 compliant segments - */ - private function setSubNamespace(?string $namespace = null, bool $append = false, bool $validate = true): void - { - if ($validate) { - $segments = explode('/', trim($namespace, '/')); - - foreach ($segments as $segment) { - if (! $this->isValidSegment($segment)) { - return; - } - } - } - - if ($append !== true || empty($this->subNamespace)) { - $this->subNamespace = trim($namespace, '/') . '\\'; - } else { - $this->subNamespace .= trim($namespace, '/') . '\\'; - } - } - private function translateURIDashes(string $classname): string { return $this->translateURIDashes From dde702f4914a627b9fe6c5c3fe0511a018855ea1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 14:36:53 +0900 Subject: [PATCH 161/485] test: add tests for fallback to default controller --- .../system/Router/AutoRouterImprovedTest.php | 39 +++++++++++++++++++ .../Router/Controllers/Subfolder/Home.php | 21 ++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/system/Router/Controllers/Subfolder/Home.php diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index 75a5f49b8648..0042ed099cb6 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -235,6 +235,45 @@ public function testAutoRouteFallbackToDefaultMethod() $this->assertSame(['15'], $params); } + public function testAutoRouteFallbackToDefaultControllerOneParam() + { + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute('subfolder/15'); + + $this->assertSame('Subfolder/', $directory); + $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('getIndex', $method); + $this->assertSame(['15'], $params); + } + + public function testAutoRouteFallbackToDefaultControllerTwoParams() + { + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute('subfolder/15/20'); + + $this->assertSame('Subfolder/', $directory); + $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('getIndex', $method); + $this->assertSame(['15', '20'], $params); + } + + public function testAutoRouteFallbackToDefaultControllerNoParams() + { + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute('subfolder'); + + $this->assertSame('Subfolder/', $directory); + $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('getIndex', $method); + $this->assertSame([], $params); + } + public function testAutoRouteRejectsSingleDot() { $this->expectException(PageNotFoundException::class); diff --git a/tests/system/Router/Controllers/Subfolder/Home.php b/tests/system/Router/Controllers/Subfolder/Home.php new file mode 100644 index 000000000000..b249971f2516 --- /dev/null +++ b/tests/system/Router/Controllers/Subfolder/Home.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Router\Controllers\Subfolder; + +use CodeIgniter\Controller; + +class Home extends Controller +{ + public function getIndex($p1 = null, $p2 = null) + { + } +} From 488c42c3cdf034632cce8a55ab88597050ab25a6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 15:43:39 +0900 Subject: [PATCH 162/485] refactor: extract method --- .../ControllerMethodReader.php | 59 ++++++++----------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php index 98e0eddfa8a1..936a1ba9c973 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php @@ -85,23 +85,7 @@ public function read(string $class, string $defaultController = 'Home', string $ continue; } - $params = []; - $routeParams = ''; - $refParams = $method->getParameters(); - - foreach ($refParams as $param) { - $required = true; - if ($param->isOptional()) { - $required = false; - - $routeParams .= '[/..]'; - } else { - $routeParams .= '/..'; - } - - // [variable_name => required?] - $params[$param->getName()] = $required; - } + [$params, $routeParams] = $this->getParameters($method); // Route for the default method. $output[] = [ @@ -117,23 +101,7 @@ public function read(string $class, string $defaultController = 'Home', string $ $route = $classInUri . '/' . $methodInUri; - $params = []; - $routeParams = ''; - $refParams = $method->getParameters(); - - foreach ($refParams as $param) { - $required = true; - if ($param->isOptional()) { - $required = false; - - $routeParams .= '[/..]'; - } else { - $routeParams .= '/..'; - } - - // [variable_name => required?] - $params[$param->getName()] = $required; - } + [$params, $routeParams] = $this->getParameters($method); // If it is the default controller, the method will not be // routed. @@ -155,6 +123,29 @@ public function read(string $class, string $defaultController = 'Home', string $ return $output; } + private function getParameters(ReflectionMethod $method): array + { + $params = []; + $routeParams = ''; + $refParams = $method->getParameters(); + + foreach ($refParams as $param) { + $required = true; + if ($param->isOptional()) { + $required = false; + + $routeParams .= '[/..]'; + } else { + $routeParams .= '/..'; + } + + // [variable_name => required?] + $params[$param->getName()] = $required; + } + + return [$params, $routeParams]; + } + /** * @phpstan-param class-string $classname * From 941a258ad7e61257016fb7baf6477c729540d7e9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 15:51:25 +0900 Subject: [PATCH 163/485] feat: parameter info for default controller --- .../ControllerMethodReader.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php index 936a1ba9c973..c91219c13a8e 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php @@ -73,7 +73,8 @@ public function read(string $class, string $defaultController = 'Home', string $ $classInUri, $classname, $methodName, - $httpVerb + $httpVerb, + $method ); if ($routeForDefaultController !== []) { @@ -180,7 +181,8 @@ private function getRouteForDefaultController( string $uriByClass, string $classname, string $methodName, - string $httpVerb + string $httpVerb, + ReflectionMethod $method ): array { $output = []; @@ -189,12 +191,18 @@ private function getRouteForDefaultController( $routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/'); $routeWithoutController = $routeWithoutController ?: '/'; + [$params, $routeParams] = $this->getParameters($method); + + if ($routeWithoutController === '/' && $routeParams !== '') { + $routeWithoutController = ''; + } + $output[] = [ 'method' => $httpVerb, 'route' => $routeWithoutController, - 'route_params' => '', + 'route_params' => $routeParams, 'handler' => '\\' . $classname . '::' . $methodName, - 'params' => [], + 'params' => $params, ]; } From b6e93adca0f858ec45df6304b30bbaa1422cf000 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 16:52:14 +0900 Subject: [PATCH 164/485] docs: add docs --- user_guide_src/source/changelogs/v4.4.0.rst | 5 ++- .../source/incoming/controllers.rst | 42 ++++++++++++++++++- .../source/incoming/controllers/025.php | 18 ++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 user_guide_src/source/incoming/controllers/025.php diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index df5b2c6da321..18713dfec84a 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -96,7 +96,10 @@ Others - **View:** Added optional 2nd parameter ``$saveData`` on ``renderSection()`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. - **Auto Routing (Improved)**: Now you can route to Modules. See :ref:`auto-routing-improved-module-routing` for details. -- **Auto Routing (Improved)**: Now you can use URI without a method name like +- **Auto Routing (Improved):** Now you can pass arguments to the the default + controller that is omitted in the URI. + See :ref:`controller-default-controller-fallback` for details. +- **Auto Routing (Improved):** Now you can use URI without a method name like ``product/15`` where ``15`` is an arbitrary number. See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index 18388e76b6a5..480b7c5f0c46 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -279,6 +279,44 @@ Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): .. literalinclude:: controllers/022.php +.. _controller-default-controller-fallback: + +Default Controller Fallback +=========================== + +.. versionadded:: 4.4.0 + +If the controller corresponding to the URI segment of the controller name +does not exist, and if the default controller (``Home`` by default) exists in +the directory, the remaining URI segments are passed to the default controller +for execution. + +For example, when you have the following default controller ``Home`` in the +**app/Controllers/News** directory: + +.. literalinclude:: controllers/025.php + +Load the following URL:: + + example.com/index.php/news/list + +The ``News\Home`` controller will be found, and the ``getList()`` method will be +executed. + +.. note:: If there is ``App\Controllers\News`` controller, it takes precedence. + The URI segments are searched sequentially and the first controller found + is used. + +Load the following URL:: + + example.com/index.php/news/101 + +The default ``getIndex()`` method will be passed URI segments 2 (``'101'``): + +.. note:: If there are more parameters in the URI than the method parameters, + Auto Routing (Improved) does not execute the method, and it results in 404 + Not Found. + .. _controller-default-method-fallback: Default Method Fallback @@ -287,8 +325,8 @@ Default Method Fallback .. versionadded:: 4.4.0 If the controller method corresponding to the URI segment of the method name -does not exist, and if the default method is defined, the URI segments are -passed to the default method for execution. +does not exist, and if the default method is defined, the remaining URI segments +are passed to the default method for execution. .. literalinclude:: controllers/024.php diff --git a/user_guide_src/source/incoming/controllers/025.php b/user_guide_src/source/incoming/controllers/025.php new file mode 100644 index 000000000000..2c88ca92ad13 --- /dev/null +++ b/user_guide_src/source/incoming/controllers/025.php @@ -0,0 +1,18 @@ + Date: Tue, 4 Apr 2023 17:26:47 +0900 Subject: [PATCH 165/485] refactor: by rector --- system/Router/AutoRouterImproved.php | 12 +++++------- tests/system/Router/AutoRouterImprovedTest.php | 6 +++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 56c524bef03d..da187a4980ad 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -248,14 +248,12 @@ public function getRoute(string $uri): array 'Cannot access the default method "' . $this->method . '" with the method name URI path.' ); } + } elseif (method_exists($this->controller, $this->defaultMethod)) { + // The default method is found. + $this->method = $this->defaultMethod; } else { - if (method_exists($this->controller, $this->defaultMethod)) { - // The default method is found. - $this->method = $this->defaultMethod; - } else { - // No method is found. - throw PageNotFoundException::forControllerNotFound($this->controller, $method); - } + // No method is found. + throw PageNotFoundException::forControllerNotFound($this->controller, $method); } // Ensure the controller is not defined in routes. diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index 0042ed099cb6..9f22260d98dc 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -243,7 +243,7 @@ public function testAutoRouteFallbackToDefaultControllerOneParam() = $router->getRoute('subfolder/15'); $this->assertSame('Subfolder/', $directory); - $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); $this->assertSame('getIndex', $method); $this->assertSame(['15'], $params); } @@ -256,7 +256,7 @@ public function testAutoRouteFallbackToDefaultControllerTwoParams() = $router->getRoute('subfolder/15/20'); $this->assertSame('Subfolder/', $directory); - $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); $this->assertSame('getIndex', $method); $this->assertSame(['15', '20'], $params); } @@ -269,7 +269,7 @@ public function testAutoRouteFallbackToDefaultControllerNoParams() = $router->getRoute('subfolder'); $this->assertSame('Subfolder/', $directory); - $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); $this->assertSame('getIndex', $method); $this->assertSame([], $params); } From fce8923bf8f1e20126fcbf49282850f869922c8d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 18:14:21 +0900 Subject: [PATCH 166/485] fix: directory is not correct --- system/Router/AutoRouterImproved.php | 16 ++++++++------ .../system/Router/AutoRouterImprovedTest.php | 13 ++++++++++++ .../Subfolder/Sub/Mycontroller.php | 21 +++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index da187a4980ad..b4a85e3648b8 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -88,7 +88,7 @@ public function __construct( string $httpVerb ) { $this->protectedControllers = $protectedControllers; - $this->namespace = rtrim($namespace, '\\') . '\\'; + $this->namespace = rtrim($namespace, '\\'); $this->translateURIDashes = $translateURIDashes; $this->httpVerb = $httpVerb; $this->defaultController = $defaultController; @@ -119,7 +119,7 @@ private function createSegments(string $uri) */ private function searchFirstController(array $segments): bool { - $controller = '\\' . trim($this->namespace, '\\'); + $controller = '\\' . $this->namespace; while ($segments !== []) { $segment = array_shift($segments); @@ -161,7 +161,7 @@ private function searchLastDefaultController(array $segments): bool $segments ); - $controller = '\\' . trim($this->namespace, '\\') + $controller = '\\' . $this->namespace . '\\' . implode('\\', $namespaces) . '\\' . $this->defaultController; @@ -177,7 +177,7 @@ private function searchLastDefaultController(array $segments): bool } // Check for the default controller in Controllers directory. - $controller = '\\' . trim($this->namespace, '\\') + $controller = '\\' . $this->namespace . '\\' . $this->defaultController; if (class_exists($controller)) { @@ -288,10 +288,14 @@ private function setDirectory() $namespaces = implode('\\', $segments); - $dir = substr($namespaces, strlen($this->namespace)); + $dir = str_replace( + '\\', + '/', + ltrim(substr($namespaces, strlen($this->namespace)), '\\') + ); if ($dir !== '') { - $this->directory = substr($namespaces, strlen($this->namespace)) . '/'; + $this->directory = $dir . '/'; } } diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index 9f22260d98dc..803407955770 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -167,6 +167,19 @@ public function testAutoRouteFindsControllerWithSubfolder() $this->assertSame([], $params); } + public function testAutoRouteFindsControllerWithSubSubfolder() + { + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute('subfolder/sub/mycontroller/somemethod'); + + $this->assertSame('Subfolder/Sub/', $directory); + $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Sub\Mycontroller::class, $controller); + $this->assertSame('getSomemethod', $method); + $this->assertSame([], $params); + } + public function testAutoRouteFindsDashedSubfolder() { $router = $this->createNewAutoRouter(); diff --git a/tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php b/tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php new file mode 100644 index 000000000000..7bd80203b914 --- /dev/null +++ b/tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Router\Controllers\Subfolder\Sub; + +use CodeIgniter\Controller; + +class Mycontroller extends Controller +{ + public function getSomemethod() + { + } +} From 4772589365594270cfc0295cbfb11f8daf2495ba Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 16:50:49 +0900 Subject: [PATCH 167/485] feat: improve error message --- system/Router/AutoRouterImproved.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index b4a85e3648b8..afa602ef9626 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -217,7 +217,7 @@ public function getRoute(string $uri): array strtolower($baseControllerName) === strtolower($this->defaultController) ) { throw new PageNotFoundException( - 'Cannot access the default controller "' . $baseControllerName . '" with the controller name URI path.' + 'Cannot access the default controller "' . $this->controller . '" with the controller name URI path.' ); } } elseif ($this->searchLastDefaultController($segments)) { From ff1e958528cd17e4597b7dc044c99fc9cafd7a2b Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 16:51:33 +0900 Subject: [PATCH 168/485] feat: prevent access to default controller's method The default method is still accesible. --- system/Router/AutoRouterImproved.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index afa602ef9626..eec6bcbd3374 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -242,6 +242,13 @@ public function getRoute(string $uri): array $this->method = $method; $this->params = $params; + // Prevent access to default controller's method + if (strtolower($baseControllerName) === strtolower($this->defaultController)) { + throw new PageNotFoundException( + 'Cannot access the default controller "' . $this->controller . '::' . $this->method . '"' + ); + } + // Prevent access to default method path if (strtolower($this->method) === strtolower($this->defaultMethod)) { throw new PageNotFoundException( From edbedc1c270dca776a78b97fe8f2181871232687 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 17:23:19 +0900 Subject: [PATCH 169/485] docs: update docs --- user_guide_src/source/changelogs/v4.4.0.rst | 9 ++- .../source/incoming/controllers.rst | 61 ++++++++----------- .../source/incoming/controllers/025.php | 5 -- 3 files changed, 28 insertions(+), 47 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 18713dfec84a..5359c4586faf 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -96,11 +96,10 @@ Others - **View:** Added optional 2nd parameter ``$saveData`` on ``renderSection()`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. - **Auto Routing (Improved)**: Now you can route to Modules. See :ref:`auto-routing-improved-module-routing` for details. -- **Auto Routing (Improved):** Now you can pass arguments to the the default - controller that is omitted in the URI. - See :ref:`controller-default-controller-fallback` for details. -- **Auto Routing (Improved):** Now you can use URI without a method name like - ``product/15`` where ``15`` is an arbitrary number. +- **Auto Routing (Improved):** If a controller is found that corresponds to a URI + segment and that controller does not have a method defined for the URI segment, + the default method will now be executed. This addition allows for more flexible + handling of URIs in auto routing. See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index 480b7c5f0c46..0f3f8d518da4 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -279,17 +279,33 @@ Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): .. literalinclude:: controllers/022.php -.. _controller-default-controller-fallback: +.. _controller-default-method-fallback: -Default Controller Fallback -=========================== +Default Method Fallback +======================= .. versionadded:: 4.4.0 +If the controller method corresponding to the URI segment of the method name +does not exist, and if the default method is defined, the remaining URI segments +are passed to the default method for execution. + +.. literalinclude:: controllers/024.php + +Load the following URL:: + + example.com/index.php/product/15/edit + +The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): + +.. important:: If there are more parameters in the URI than the method parameters, + Auto Routing (Improved) does not execute the method, and it results in 404 + Not Found. + If the controller corresponding to the URI segment of the controller name does not exist, and if the default controller (``Home`` by default) exists in -the directory, the remaining URI segments are passed to the default controller -for execution. +the directory, the remaining URI segments are passed to the default controller's +default method. For example, when you have the following default controller ``Home`` in the **app/Controllers/News** directory: @@ -298,48 +314,19 @@ For example, when you have the following default controller ``Home`` in the Load the following URL:: - example.com/index.php/news/list + example.com/index.php/news/101 -The ``News\Home`` controller will be found, and the ``getList()`` method will be -executed. +The ``News\Home`` controller and the default ``getIndex()`` method will be found. +So the default method will be passed URI segments 2 (``'101'``): .. note:: If there is ``App\Controllers\News`` controller, it takes precedence. The URI segments are searched sequentially and the first controller found is used. -Load the following URL:: - - example.com/index.php/news/101 - -The default ``getIndex()`` method will be passed URI segments 2 (``'101'``): - .. note:: If there are more parameters in the URI than the method parameters, Auto Routing (Improved) does not execute the method, and it results in 404 Not Found. -.. _controller-default-method-fallback: - -Default Method Fallback -======================= - -.. versionadded:: 4.4.0 - -If the controller method corresponding to the URI segment of the method name -does not exist, and if the default method is defined, the remaining URI segments -are passed to the default method for execution. - -.. literalinclude:: controllers/024.php - -Load the following URL:: - - example.com/index.php/product/15/edit - -The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): - -.. important:: If there are more parameters in the URI than the method parameters, - Auto Routing (Improved) does not execute the method, and it results in 404 - Not Found. - Default Controller ================== diff --git a/user_guide_src/source/incoming/controllers/025.php b/user_guide_src/source/incoming/controllers/025.php index 2c88ca92ad13..732c6db94c01 100644 --- a/user_guide_src/source/incoming/controllers/025.php +++ b/user_guide_src/source/incoming/controllers/025.php @@ -6,11 +6,6 @@ class Home extends BaseController { - public function getList() - { - // ... - } - public function getIndex($id = null) { // ... From d96f0ad514fd3e6249aeb49eefa4ce17d5aaba3e Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 16:25:28 +0900 Subject: [PATCH 170/485] docs: move section up --- .../source/incoming/controllers.rst | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index 0f3f8d518da4..03ef53c8e3cd 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -279,6 +279,40 @@ Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): .. literalinclude:: controllers/022.php +Default Controller +================== + +The Default Controller is a special controller that is used when a URI ends with +a directory name or when a URI is not present, as will be the case when only your +site root URL is requested. + +Defining a Default Controller +----------------------------- + +Let's try it with the ``Helloworld`` controller. + +To specify a default controller open your **app/Config/Routes.php** +file and set this variable: + +.. literalinclude:: controllers/015.php + +Where ``Helloworld`` is the name of the controller class you want to be used. + +A few lines further down **Routes.php** in the "Route Definitions" section, comment out the line: + +.. literalinclude:: controllers/016.php + +If you now browse to your site without specifying any URI segments you'll +see the "Hello World" message. + +.. important:: When you use Auto Routing (Improved), you must remove the line + ``$routes->get('/', 'Home::index');``. Because defined routes take + precedence over Auto Routing, and controllers defined in the defined routes + are denied access by Auto Routing (Improved) for security reasons. + +For more information, please refer to the :ref:`routes-configuration-options` section of the +:ref:`URI Routing ` documentation. + .. _controller-default-method-fallback: Default Method Fallback @@ -327,40 +361,6 @@ So the default method will be passed URI segments 2 (``'101'``): Auto Routing (Improved) does not execute the method, and it results in 404 Not Found. -Default Controller -================== - -The Default Controller is a special controller that is used when a URI ends with -a directory name or when a URI is not present, as will be the case when only your -site root URL is requested. - -Defining a Default Controller ------------------------------ - -Let's try it with the ``Helloworld`` controller. - -To specify a default controller open your **app/Config/Routes.php** -file and set this variable: - -.. literalinclude:: controllers/015.php - -Where ``Helloworld`` is the name of the controller class you want to be used. - -A few lines further down **Routes.php** in the "Route Definitions" section, comment out the line: - -.. literalinclude:: controllers/016.php - -If you now browse to your site without specifying any URI segments you'll -see the "Hello World" message. - -.. important:: When you use Auto Routing (Improved), you must remove the line - ``$routes->get('/', 'Home::index');``. Because defined routes take - precedence over Auto Routing, and controllers defined in the defined routes - are denied access by Auto Routing (Improved) for security reasons. - -For more information, please refer to the :ref:`routes-configuration-options` section of the -:ref:`URI Routing ` documentation. - Organizing Your Controllers into Sub-directories ================================================ From cc6b2d9bbbf8526f348561a66a973b2956e5d523 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 16:28:16 +0900 Subject: [PATCH 171/485] docs: add section title --- user_guide_src/source/incoming/controllers.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index 03ef53c8e3cd..8bc9e90d581f 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -336,6 +336,9 @@ The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): Auto Routing (Improved) does not execute the method, and it results in 404 Not Found. +Fallback to Default Controller +------------------------------ + If the controller corresponding to the URI segment of the controller name does not exist, and if the default controller (``Home`` by default) exists in the directory, the remaining URI segments are passed to the default controller's From 15ee8d64b2fa78aef6547b49083c9eb9de013c01 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 19:23:56 +0900 Subject: [PATCH 172/485] fix: Fatal error Cannot declare class CodeIgniter\Router\Controllers\Index --- system/Router/AutoRouterImproved.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index eec6bcbd3374..0999f47eb26b 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -201,11 +201,12 @@ public function getRoute(string $uri): array // Check for Module Routes. if ( - ($routingConfig = config(Routing::class)) + $segments !== [] + && ($routingConfig = config(Routing::class)) && array_key_exists($segments[0], $routingConfig->moduleRoutes) ) { $uriSegment = array_shift($segments); - $this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\') . '\\'; + $this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\'); } if ($this->searchFirstController($segments)) { From f4fbbc5a60cdb695ce1055b7154a818f8669a780 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Fri, 14 Apr 2023 23:27:27 +0200 Subject: [PATCH 173/485] array_group_by and unit tests --- system/Helpers/array_helper.php | 46 ++ tests/system/Helpers/ArrayHelperTest.php | 794 +++++++++++++++++++++++ 2 files changed, 840 insertions(+) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 9b88cc2a0e0f..7c5c6d2c5de2 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -218,3 +218,49 @@ function array_flatten_with_dots(iterable $array, string $id = ''): array return $flattened; } } + +if (! function_exists('array_group_by')) { + /** + * Groups all rows by their index values. Result's depth equals number of indexes + * + * @param array $array Data array (i.e. from query result) + * @param array $indexes Indexes to group by. Dot syntax used. Returns $array if empty + * @param bool $includeEmpty If true, null and '' are also added as valid keys to group + * + * @return array Result array where rows are grouped together by indexes values. + */ + function array_group_by(array $array, array $indexes, bool $includeEmpty = false): array + { + if (empty($indexes)) { + return $array; + } + + $result = []; + + foreach ($array as $row) { + $currentLevel = &$result; + $valid = true; + + foreach ($indexes as $index) { + $value = dot_array_search($index, $row); + + if (! $includeEmpty && empty($value)) { + $valid = false; + break; + } + + if (! array_key_exists($value, $currentLevel)) { + $currentLevel[$value] = []; + } + + $currentLevel = &$currentLevel[$value]; + } + + if ($valid) { + $currentLevel[] = $row; + } + } + + return $result; + } +} diff --git a/tests/system/Helpers/ArrayHelperTest.php b/tests/system/Helpers/ArrayHelperTest.php index a2db5b06e7d7..e19c410a32cd 100644 --- a/tests/system/Helpers/ArrayHelperTest.php +++ b/tests/system/Helpers/ArrayHelperTest.php @@ -498,4 +498,798 @@ public function arrayFlattenProvider(): iterable ], ]; } + + /** + * @dataProvider arrayGroupByIncludeEmptyProvider + */ + public function testArrayGroupByIncludeEmpty(array $indexes, array $data, array $expected): void + { + $actual = array_group_by($data, $indexes, true); + + $this->assertSame($expected, $actual, 'array including empty not the same'); + } + + /** + * @dataProvider arrayGroupByExcludeEmptyProvider + */ + public function testArrayGroupByExcludeEmpty(array $indexes, array $data, array $expected): void + { + $actual = array_group_by($data, $indexes, false); + + $this->assertSame($expected, $actual, 'array excluding empty not the same'); + } + + public function arrayGroupByIncludeEmptyProvider(): iterable + { + yield 'simple group-by test' => [ + ['color'], + [ + [ + 'id' => 1, + 'item' => 'ball', + 'color' => 'blue', + ], + [ + 'id' => 2, + 'item' => 'book', + 'color' => 'red', + ], + [ + 'id' => 3, + 'item' => 'bird', + 'age' => 5, + ], + [ + 'id' => 4, + 'item' => 'jeans', + 'color' => 'blue', + ], + ], + [ + 'blue' => [ + [ + 'id' => 1, + 'item' => 'ball', + 'color' => 'blue', + ], + [ + 'id' => 4, + 'item' => 'jeans', + 'color' => 'blue', + ], + ], + 'red' => [ + [ + 'id' => 2, + 'item' => 'book', + 'color' => 'red', + ], + ], + '' => [ + [ + 'id' => 3, + 'item' => 'bird', + 'age' => 5, + ], + ], + ], + ]; + + yield '2 index data' => [ + ['gender', 'country'], + [ + [ + 'id' => 1, + 'first_name' => 'Scarface', + 'gender' => 'Male', + 'country' => 'Germany', + ], + [ + 'id' => 2, + 'first_name' => 'Fletch', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 3, + 'first_name' => 'Wrennie', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 4, + 'first_name' => 'Virgilio', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 5, + 'first_name' => 'Cathlene', + 'gender' => 'Polygender', + 'country' => 'France', + ], + [ + 'id' => 6, + 'first_name' => 'Far', + 'gender' => 'Male', + 'country' => 'Canada', + ], + [ + 'id' => 7, + 'first_name' => 'Dolores', + 'gender' => 'Female', + 'country' => 'Canada', + ], + [ + 'id' => 8, + 'first_name' => 'Sissy', + 'gender' => 'Female', + 'country' => null, + ], + [ + 'id' => 9, + 'first_name' => 'Chlo', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 10, + 'first_name' => 'Gabbie', + 'gender' => 'Male', + 'country' => 'Canada', + ], + ], + [ + 'Male' => [ + 'Germany' => [ + [ + 'id' => 1, + 'first_name' => 'Scarface', + 'gender' => 'Male', + 'country' => 'Germany', + ], + ], + 'France' => [ + [ + 'id' => 2, + 'first_name' => 'Fletch', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 4, + 'first_name' => 'Virgilio', + 'gender' => 'Male', + 'country' => 'France', + ], + ], + 'Canada' => [ + [ + 'id' => 6, + 'first_name' => 'Far', + 'gender' => 'Male', + 'country' => 'Canada', + ], + [ + 'id' => 10, + 'first_name' => 'Gabbie', + 'gender' => 'Male', + 'country' => 'Canada', + ], + ], + ], + 'Female' => [ + 'France' => [ + [ + 'id' => 3, + 'first_name' => 'Wrennie', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 9, + 'first_name' => 'Chlo', + 'gender' => 'Female', + 'country' => 'France', + ], + ], + 'Canada' => [ + [ + 'id' => 7, + 'first_name' => 'Dolores', + 'gender' => 'Female', + 'country' => 'Canada', + ], + ], + '' => [ + [ + 'id' => 8, + 'first_name' => 'Sissy', + 'gender' => 'Female', + 'country' => null, + ], + ], + ], + 'Polygender' => [ + 'France' => [ + [ + 'id' => 5, + 'first_name' => 'Cathlene', + 'gender' => 'Polygender', + 'country' => 'France', + ], + ], + ], + ], + ]; + + yield 'nested data with dot syntax' => [ + ['gender', 'hr.department'], + [ + [ + 'id' => 1, + 'first_name' => 'Urbano', + 'gender' => null, + 'hr' => [ + 'country' => 'Canada', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 4, + 'first_name' => 'Richy', + 'gender' => null, + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 5, + 'first_name' => 'Mandy', + 'gender' => null, + 'hr' => [ + 'country' => 'France', + 'department' => 'Sales', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + [ + '' => [ + 'Engineering' => [ + [ + 'id' => 1, + 'first_name' => 'Urbano', + 'gender' => null, + 'hr' => [ + 'country' => 'Canada', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 4, + 'first_name' => 'Richy', + 'gender' => null, + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 5, + 'first_name' => 'Mandy', + 'gender' => null, + 'hr' => [ + 'country' => 'France', + 'department' => 'Sales', + ], + ], + ], + ], + 'Male' => [ + 'Marketing' => [ + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + 'Engineering' => [ + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + ], + ], + 'Female' => [ + 'Engineering' => [ + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + ], + ], + ], + ]; + } + + public function arrayGroupByExcludeEmptyProvider(): iterable + { + yield 'simple group-by test' => [ + ['color'], + [ + [ + 'id' => 1, + 'item' => 'ball', + 'color' => 'blue', + ], + [ + 'id' => 2, + 'item' => 'book', + 'color' => 'red', + ], + [ + 'id' => 3, + 'item' => 'bird', + 'age' => 5, + ], + [ + 'id' => 4, + 'item' => 'jeans', + 'color' => 'blue', + ], + ], + [ + 'blue' => [ + [ + 'id' => 1, + 'item' => 'ball', + 'color' => 'blue', + ], + [ + 'id' => 4, + 'item' => 'jeans', + 'color' => 'blue', + ], + ], + 'red' => [ + [ + 'id' => 2, + 'item' => 'book', + 'color' => 'red', + ], + ], + ], + ]; + + yield '2 index data' => [ + ['gender', 'country'], + [ + [ + 'id' => 1, + 'first_name' => 'Scarface', + 'gender' => 'Male', + 'country' => 'Germany', + ], + [ + 'id' => 2, + 'first_name' => 'Fletch', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 3, + 'first_name' => 'Wrennie', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 4, + 'first_name' => 'Virgilio', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 5, + 'first_name' => 'Cathlene', + 'gender' => 'Polygender', + 'country' => 'France', + ], + [ + 'id' => 6, + 'first_name' => 'Far', + 'gender' => 'Male', + 'country' => 'Canada', + ], + [ + 'id' => 7, + 'first_name' => 'Dolores', + 'gender' => 'Female', + 'country' => 'Canada', + ], + [ + 'id' => 8, + 'first_name' => 'Sissy', + 'gender' => 'Female', + 'country' => null, + ], + [ + 'id' => 9, + 'first_name' => 'Chlo', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 10, + 'first_name' => 'Gabbie', + 'gender' => 'Male', + 'country' => 'Canada', + ], + ], + [ + 'Male' => [ + 'Germany' => [ + [ + 'id' => 1, + 'first_name' => 'Scarface', + 'gender' => 'Male', + 'country' => 'Germany', + ], + ], + 'France' => [ + [ + 'id' => 2, + 'first_name' => 'Fletch', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 4, + 'first_name' => 'Virgilio', + 'gender' => 'Male', + 'country' => 'France', + ], + ], + 'Canada' => [ + [ + 'id' => 6, + 'first_name' => 'Far', + 'gender' => 'Male', + 'country' => 'Canada', + ], + [ + 'id' => 10, + 'first_name' => 'Gabbie', + 'gender' => 'Male', + 'country' => 'Canada', + ], + ], + ], + 'Female' => [ + 'France' => [ + [ + 'id' => 3, + 'first_name' => 'Wrennie', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 9, + 'first_name' => 'Chlo', + 'gender' => 'Female', + 'country' => 'France', + ], + ], + 'Canada' => [ + [ + 'id' => 7, + 'first_name' => 'Dolores', + 'gender' => 'Female', + 'country' => 'Canada', + ], + ], + ], + 'Polygender' => [ + 'France' => [ + [ + 'id' => 5, + 'first_name' => 'Cathlene', + 'gender' => 'Polygender', + 'country' => 'France', + ], + ], + ], + ], + ]; + + yield 'nested data with dot syntax' => [ + ['gender', 'hr.department'], + [ + [ + 'id' => 1, + 'first_name' => 'Urbano', + 'gender' => null, + 'hr' => [ + 'country' => 'Canada', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 4, + 'first_name' => 'Richy', + 'gender' => null, + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 5, + 'first_name' => 'Mandy', + 'gender' => null, + 'hr' => [ + 'country' => 'France', + 'department' => 'Sales', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + [ + 'Male' => [ + 'Marketing' => [ + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + 'Engineering' => [ + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + ], + ], + 'Female' => [ + 'Engineering' => [ + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + ], + ], + ], + ]; + } } From 76993a5fb02dadf044d48a633d0b369aacaf8dd8 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sat, 15 Apr 2023 00:11:38 +0200 Subject: [PATCH 174/485] Documentation --- .../source/helpers/array_helper.rst | 24 ++++ .../source/helpers/array_helper/012.php | 94 +++++++++++++++ .../source/helpers/array_helper/013.php | 81 +++++++++++++ .../source/helpers/array_helper/014.php | 114 ++++++++++++++++++ 4 files changed, 313 insertions(+) create mode 100644 user_guide_src/source/helpers/array_helper/012.php create mode 100644 user_guide_src/source/helpers/array_helper/013.php create mode 100644 user_guide_src/source/helpers/array_helper/014.php diff --git a/user_guide_src/source/helpers/array_helper.rst b/user_guide_src/source/helpers/array_helper.rst index cf9517925234..730dae957222 100644 --- a/user_guide_src/source/helpers/array_helper.rst +++ b/user_guide_src/source/helpers/array_helper.rst @@ -111,3 +111,27 @@ The following functions are available: will be supplying an initial ``$id``, it will be prepended to all keys. .. literalinclude:: array_helper/011.php + +.. php:function:: array_group_by(array $array, array $indexes[, bool $includeEmpty = false]): array + + :param array $array: Data rows (most likely from query results) + :param array $indexes: Indexes to group values. Follows dot syntax + :param bool $includeEmpty: If true, ``null`` and ``''`` values are not filtered out + :rtype: array + :returns: An array grouped by indexes values + + This function allows you to group data rows together by index values. + The depth of returned array equals the number of indexes passed as parameter. + + The example shows some data (i.e. loaded from an API) with nested arrays. + + .. literalinclude:: array_helper/012.php + + We want to group them first by "gender", then by "hr.department" (max depth = 2). + First the result when excluding empty values: + + .. literalinclude:: array_helper/013.php + + And here the same code, but this time we want to include empty values: + + .. literalinclude:: array_helper/014.php diff --git a/user_guide_src/source/helpers/array_helper/012.php b/user_guide_src/source/helpers/array_helper/012.php new file mode 100644 index 000000000000..8aeb6d705c7d --- /dev/null +++ b/user_guide_src/source/helpers/array_helper/012.php @@ -0,0 +1,94 @@ + 1, + 'first_name' => 'Urbano', + 'gender' => null, + 'hr' => [ + 'country' => 'Canada', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 4, + 'first_name' => 'Richy', + 'gender' => null, + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 5, + 'first_name' => 'Mandy', + 'gender' => null, + 'hr' => [ + 'country' => 'France', + 'department' => 'Sales', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], +]; diff --git a/user_guide_src/source/helpers/array_helper/013.php b/user_guide_src/source/helpers/array_helper/013.php new file mode 100644 index 000000000000..5b7e722fce75 --- /dev/null +++ b/user_guide_src/source/helpers/array_helper/013.php @@ -0,0 +1,81 @@ + [ + 'Marketing' => [ + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + 'Engineering' => [ + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + ], + ], + 'Female' => [ + 'Engineering' => [ + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + ], + ], +]; diff --git a/user_guide_src/source/helpers/array_helper/014.php b/user_guide_src/source/helpers/array_helper/014.php new file mode 100644 index 000000000000..99089d236598 --- /dev/null +++ b/user_guide_src/source/helpers/array_helper/014.php @@ -0,0 +1,114 @@ + [ + 'Engineering' => [ + [ + 'id' => 1, + 'first_name' => 'Urbano', + 'gender' => null, + 'hr' => [ + 'country' => 'Canada', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 4, + 'first_name' => 'Richy', + 'gender' => null, + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 5, + 'first_name' => 'Mandy', + 'gender' => null, + 'hr' => [ + 'country' => 'France', + 'department' => 'Sales', + ], + ], + ], + ], + 'Male' => [ + 'Marketing' => [ + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + 'Engineering' => [ + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + ], + ], + 'Female' => [ + 'Engineering' => [ + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + ], + ], +]; From a3bc77ed088807e7d49d6733d4fd68d1fbbdb713 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sat, 15 Apr 2023 10:26:12 +0200 Subject: [PATCH 175/485] strict comparison, phpstan fixes --- system/Helpers/array_helper.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 7c5c6d2c5de2..6de04726eaaa 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -231,7 +231,7 @@ function array_flatten_with_dots(iterable $array, string $id = ''): array */ function array_group_by(array $array, array $indexes, bool $includeEmpty = false): array { - if (empty($indexes)) { + if ($indexes === []) { return $array; } @@ -244,12 +244,16 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false foreach ($indexes as $index) { $value = dot_array_search($index, $row); - if (! $includeEmpty && empty($value)) { + if (is_array($value) || is_object($value) || (! $includeEmpty && ($value === null || $value === ''))) { $valid = false; break; } - if (! array_key_exists($value, $currentLevel)) { + if ($value === null) { + $value = ''; + } + + if (! isset($currentLevel[$value])) { $currentLevel[$value] = []; } From 0b3a04e8211e78c2d2046bd6c723af58a77745bb Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sat, 15 Apr 2023 12:33:12 +0200 Subject: [PATCH 176/485] logic improvements, phpstan fixes --- system/Helpers/array_helper.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 6de04726eaaa..02c0afbcea82 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -244,19 +244,23 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false foreach ($indexes as $index) { $value = dot_array_search($index, $row); - if (is_array($value) || is_object($value) || (! $includeEmpty && ($value === null || $value === ''))) { - $valid = false; - break; - } - - if ($value === null) { + if (is_array($value) || is_object($value) || $value === null) { $value = ''; } - if (! isset($currentLevel[$value])) { - $currentLevel[$value] = []; + if (is_bool($value)) { + $value = intval($value); + } + + if (! $includeEmpty && $value === '') { + $valid = false; + break; } + if (! array_key_exists($value, $currentLevel)) { + $currentLevel[$value] = []; + } + $currentLevel = &$currentLevel[$value]; } From c2a7402a81d8b6350fe4b8e348d1ee4216ded3a2 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sat, 15 Apr 2023 13:06:39 +0200 Subject: [PATCH 177/485] cs fix, always use string value --- system/Helpers/array_helper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 02c0afbcea82..aa392e98bc03 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -258,8 +258,8 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false } if (! array_key_exists($value, $currentLevel)) { - $currentLevel[$value] = []; - } + $currentLevel[$value] = []; + } $currentLevel = &$currentLevel[$value]; } From 443fd77302e7314c68f235d14f989d87ed1af338 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sat, 15 Apr 2023 13:18:07 +0200 Subject: [PATCH 178/485] additional CS fixes --- system/Helpers/array_helper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index aa392e98bc03..e89b99cd99a4 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -249,8 +249,8 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false } if (is_bool($value)) { - $value = intval($value); - } + $value = (int) $value; + } if (! $includeEmpty && $value === '') { $valid = false; From 8bff019e69d97f416f7aae147316d8b5d5859cb0 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sun, 16 Apr 2023 19:29:42 +0200 Subject: [PATCH 179/485] phpstan ignore false-positive error --- system/Helpers/array_helper.php | 1 + 1 file changed, 1 insertion(+) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index e89b99cd99a4..2245606d1b25 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -257,6 +257,7 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false break; } + // @phpstan-ignore-next-line if (! array_key_exists($value, $currentLevel)) { $currentLevel[$value] = []; } From 752fbbebc28f747e79a1acf2b96e8702434a8c9c Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sun, 16 Apr 2023 20:56:56 +0200 Subject: [PATCH 180/485] Recursive approach with internal function --- system/Helpers/array_helper.php | 54 +++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 2245606d1b25..74d62b081f59 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -238,38 +238,46 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false $result = []; foreach ($array as $row) { - $currentLevel = &$result; - $valid = true; + $result = _array_attach_indexed_value($result, $row, $indexes, $includeEmpty); + } - foreach ($indexes as $index) { - $value = dot_array_search($index, $row); + return $result; + } +} - if (is_array($value) || is_object($value) || $value === null) { - $value = ''; - } +if (!function_exists('_array_attach_indexed_value')) { + /** + * Used by `array_group_by` to recursively attach $row to the $indexes path of values found by + * `dot_array_search` + * + * @internal This should not be used on its own + */ + function _array_attach_indexed_value(array $result, array $row, array $indexes, bool $includeEmpty): array + { + if (($index = array_shift($indexes)) === null) { + $result[] = $row; + return $result; + } - if (is_bool($value)) { - $value = (int) $value; - } + $value = dot_array_search($index, $row); - if (! $includeEmpty && $value === '') { - $valid = false; - break; - } + if (is_array($value) || is_object($value) || $value === null) { + $value = ''; + } - // @phpstan-ignore-next-line - if (! array_key_exists($value, $currentLevel)) { - $currentLevel[$value] = []; - } + if (is_bool($value)) { + $value = (int) $value; + } - $currentLevel = &$currentLevel[$value]; - } + if (! $includeEmpty && $value === '') { + return $result; + } - if ($valid) { - $currentLevel[] = $row; - } + if (! array_key_exists($value, $result)) { + $result[$value] = []; } + $result[$value] = _array_attach_indexed_value($result[$value], $row, $indexes, $includeEmpty); return $result; } } From 2dd683d0c1a9a74fd921a8582f7a6f41ffa9cae7 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sun, 16 Apr 2023 21:17:30 +0200 Subject: [PATCH 181/485] CS fixes --- system/Helpers/array_helper.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 74d62b081f59..2ed71cf7b29d 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -245,7 +245,7 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false } } -if (!function_exists('_array_attach_indexed_value')) { +if (! function_exists('_array_attach_indexed_value')) { /** * Used by `array_group_by` to recursively attach $row to the $indexes path of values found by * `dot_array_search` @@ -256,6 +256,7 @@ function _array_attach_indexed_value(array $result, array $row, array $indexes, { if (($index = array_shift($indexes)) === null) { $result[] = $row; + return $result; } @@ -278,6 +279,7 @@ function _array_attach_indexed_value(array $result, array $row, array $indexes, } $result[$value] = _array_attach_indexed_value($result[$value], $row, $indexes, $includeEmpty); + return $result; } } From 4e69051d0cb0d43bcf3e3a06469e6d2b24eea604 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Wed, 26 Apr 2023 19:56:16 +0200 Subject: [PATCH 182/485] Added entry in changelogs --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index df5b2c6da321..1385d24fb104 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -103,6 +103,7 @@ Others - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. - **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with headings. See :ref:`table-sync-rows-with-headings` for details. - **Error Handling:** Now you can use :ref:`custom-exception-handlers`. +- **Array:** Added ``array_group_by()`` helper function to group data values together. Supports dot-notation syntax. Message Changes *************** From 84a89adc1c9e7e83a0eafcf41ecb3a4cf735a265 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Thu, 27 Apr 2023 20:09:35 +0200 Subject: [PATCH 183/485] Fixed changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 1385d24fb104..388690daecfe 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -87,6 +87,9 @@ Libraries Helpers and Functions ===================== +- **Array:** Added :php:func:`array_group_by()` helper function to group data + values together. Supports dot-notation syntax. + Others ====== @@ -103,7 +106,6 @@ Others - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. - **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with headings. See :ref:`table-sync-rows-with-headings` for details. - **Error Handling:** Now you can use :ref:`custom-exception-handlers`. -- **Array:** Added ``array_group_by()`` helper function to group data values together. Supports dot-notation syntax. Message Changes *************** From 0e86d4cba3b86c5247bd2471588142aeef5b06d5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 16:13:06 +0900 Subject: [PATCH 184/485] test: add test for Fore::modifyColumn() and null --- tests/system/Database/Live/ForgeTest.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 5eb9d3ba85fd..778c54ab3d1a 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -1285,11 +1285,6 @@ public function testModifyColumnRename() public function testModifyColumnNullTrue() { - // @TODO remove this in `4.4` branch - if ($this->db->DBDriver === 'SQLSRV') { - $this->markTestSkipped('SQLSRV does not support getFieldData() nullable.'); - } - $this->forge->dropTable('forge_test_modify', true); $this->forge->addField([ @@ -1319,11 +1314,6 @@ public function testModifyColumnNullTrue() public function testModifyColumnNullFalse() { - // @TODO remove this in `4.4` branch - if ($this->db->DBDriver === 'SQLSRV') { - $this->markTestSkipped('SQLSRV does not support getFieldData() nullable.'); - } - $this->forge->dropTable('forge_test_modify', true); $this->forge->addField([ From 5a977fcbab02d0424d4552ad4614ffa6303c60e3 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Mon, 1 May 2023 19:04:36 +0200 Subject: [PATCH 185/485] Using is_scalar --- system/Helpers/array_helper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 2ed71cf7b29d..632e8a3a6c7c 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -262,7 +262,7 @@ function _array_attach_indexed_value(array $result, array $row, array $indexes, $value = dot_array_search($index, $row); - if (is_array($value) || is_object($value) || $value === null) { + if (! is_scalar($value)) { $value = ''; } From f251ae27c46883cea42acc3d9988410457642aae Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 1 Mar 2023 16:03:09 +0900 Subject: [PATCH 186/485] test: add test for Forge::addColumn() and null --- tests/system/Database/Live/ForgeTest.php | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 778c54ab3d1a..026e024e098c 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -827,6 +827,41 @@ public function testAddColumn() $this->assertSame('username', $fieldNames[1]); } + public function testAddColumnNull() + { + $this->forge->dropTable('forge_test_table', true); + + $this->forge->addField([ + 'col1' => ['type' => 'VARCHAR', 'constraint' => 255], + 'col2' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'col3' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false], + ]); + $this->forge->createTable('forge_test_table'); + + $this->forge->addColumn('forge_test_table', [ + 'col4' => ['type' => 'VARCHAR', 'constraint' => 255], + 'col5' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'col6' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false], + ]); + + $this->db->resetDataCache(); + + $col1 = $this->getMetaData('col1', 'forge_test_table'); + $this->assertFalse($col1->nullable); + $col2 = $this->getMetaData('col2', 'forge_test_table'); + $this->assertTrue($col2->nullable); + $col3 = $this->getMetaData('col3', 'forge_test_table'); + $this->assertFalse($col3->nullable); + $col4 = $this->getMetaData('col4', 'forge_test_table'); + $this->assertTrue($col4->nullable); + $col5 = $this->getMetaData('col5', 'forge_test_table'); + $this->assertTrue($col5->nullable); + $col6 = $this->getMetaData('col6', 'forge_test_table'); + $this->assertFalse($col6->nullable); + + $this->forge->dropTable('forge_test_table', true); + } + public function testAddFields() { $tableName = 'forge_test_fields'; From ee2a911c94c58633eb5297c41c9f09dc404eb512 Mon Sep 17 00:00:00 2001 From: ping-yee <611077101@mail.nknu.edu.tw> Date: Tue, 9 May 2023 18:30:50 +0800 Subject: [PATCH 187/485] fix: fix the path string. --- system/Router/RouteCollection.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 97f85400cdee..d1731b32c270 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -264,6 +264,11 @@ public function __construct(FileLocator $locator, Modules $moduleConfig, Routing $this->autoRoute = $routing->autoRoute; $this->routeFiles = $routing->routeFiles; $this->prioritize = $routing->prioritize; + + // Normalize the path string in routeFiles array. + foreach ($this->routeFiles as $routeKey => $routesFile) { + $this->routeFiles[$routeKey] = realpath($routesFile) ?: $routesFile; + } } /** From 96a0d4f303347a7f1c881fe7d09fb8a2478e61f2 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Mon, 15 May 2023 14:58:30 +0800 Subject: [PATCH 188/485] fix: output buffering --- system/CodeIgniter.php | 56 ++++++++++++--------- system/Debug/BaseExceptionHandler.php | 4 -- system/Debug/Exceptions.php | 4 -- tests/system/CodeIgniterTest.php | 18 ++++--- user_guide_src/source/changelogs/v4.4.0.rst | 2 + 5 files changed, 45 insertions(+), 39 deletions(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 5e6c932aa2ea..54d89cce6eaf 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -36,6 +36,7 @@ use Kint\Renderer\RichRenderer; use Locale; use LogicException; +use Throwable; /** * This class is the core of the framework, and will analyse the @@ -164,6 +165,11 @@ class CodeIgniter */ protected bool $returnResponse = false; + /** + * Application output buffering level + */ + protected int $bufferLevel; + /** * Constructor. */ @@ -367,6 +373,7 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon try { return $this->handleRequest($routes, $cacheConfig, $returnResponse); } catch (RedirectException $e) { + $this->outputBufferingEnd(); $logger = Services::logger(); $logger->info('REDIRECTED ROUTE at ' . $e->getMessage()); @@ -389,6 +396,10 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon if ($return instanceof ResponseInterface) { return $return; } + } catch (Throwable $e) { + $this->outputBufferingEnd(); + + throw $e; } } @@ -810,7 +821,7 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null) $this->benchmark->stop('bootstrap'); $this->benchmark->start('routing'); - ob_start(); + $this->outputBufferingStart(); $this->controller = $this->router->handle($path); $this->method = $this->router->methodName(); @@ -979,15 +990,8 @@ protected function display404errors(PageNotFoundException $e) // Display 404 Errors $this->response->setStatusCode($e->getCode()); - if (ENVIRONMENT !== 'testing') { - if (ob_get_level() > 0) { - ob_end_flush(); // @codeCoverageIgnore - } - } - // When testing, one is for phpunit, another is for test case. - elseif (ob_get_level() > 2) { - ob_end_flush(); // @codeCoverageIgnore - } + echo $this->outputBufferingEnd(); + flush(); // Throws new PageNotFoundException and remove exception message on production. throw PageNotFoundException::forPageNotFound( @@ -1006,21 +1010,9 @@ protected function display404errors(PageNotFoundException $e) */ protected function gatherOutput(?Cache $cacheConfig = null, $returned = null) { - $this->output = ob_get_contents(); - // If buffering is not null. - // Clean (erase) the output buffer and turn off output buffering - if (ob_get_length()) { - ob_end_clean(); - } + $this->output = $this->outputBufferingEnd(); if ($returned instanceof DownloadResponse) { - // Turn off output buffering completely, even if php.ini output_buffering is not off - if (ENVIRONMENT !== 'testing') { - while (ob_get_level() > 0) { - ob_end_clean(); - } - } - $this->response = $returned; return; @@ -1150,4 +1142,22 @@ public function setContext(string $context) return $this; } + + protected function outputBufferingStart(): void + { + $this->bufferLevel = ob_get_level(); + ob_start(); + } + + protected function outputBufferingEnd(): string + { + $buffer = ''; + + while (ob_get_level() > $this->bufferLevel) { + $buffer = ob_get_contents(); + ob_end_clean(); + } + + return $buffer; + } } diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php index a4a4c72946b1..576149b8bf68 100644 --- a/system/Debug/BaseExceptionHandler.php +++ b/system/Debug/BaseExceptionHandler.php @@ -215,10 +215,6 @@ protected function render(Throwable $exception, int $statusCode, $viewFile = nul exit(1); } - if (ob_get_level() > $this->obLevel + 1) { - ob_end_clean(); - } - echo(function () use ($exception, $statusCode, $viewFile): string { $vars = $this->collectVars($exception, $statusCode); extract($vars, EXTR_SKIP); diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 9d2ada166aee..8979febf4d46 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -296,10 +296,6 @@ protected function render(Throwable $exception, int $statusCode) exit(1); } - if (ob_get_level() > $this->ob_level + 1) { - ob_end_clean(); - } - echo(function () use ($exception, $statusCode, $viewFile): string { $vars = $this->collectVars($exception, $statusCode); extract($vars, EXTR_SKIP); diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 2871b3ffdb52..cef482bb4e34 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -54,10 +54,6 @@ protected function tearDown(): void { parent::tearDown(); - if (ob_get_level() > 1) { - ob_end_clean(); - } - $this->resetServices(); } @@ -73,6 +69,16 @@ public function testRunEmptyDefaultRoute() $this->assertStringContainsString('Welcome to CodeIgniter', $output); } + public function testOutputBufferingControl() + { + ob_start(); + $this->codeigniter->run(); + ob_get_clean(); + + // 1 phpunit output buffering level + $this->assertSame(1, ob_get_level()); + } + public function testRunEmptyDefaultRouteReturnResponse() { $_SERVER['argv'] = ['index.php']; @@ -828,10 +834,6 @@ public function testPageCacheWithCacheQueryString( $output = ob_get_clean(); $this->assertSame($string, $output); - - if (ob_get_level() > 1) { - ob_end_clean(); - } } // Calculate how much cached items exist in the cache after the test requests diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 652f4f03c579..ebfa65ede545 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -142,6 +142,8 @@ Deprecations Bugs Fixed ********** +- **Output Buffering:** Bug fix with output buffering. + See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. From 2033cea9b21087e4e308e7c00c2a542682ae0201 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Mon, 15 May 2023 19:33:45 +0800 Subject: [PATCH 189/485] testing and bug fixes. --- system/CodeIgniter.php | 2 +- system/Test/FeatureTestTrait.php | 22 ---------------------- tests/system/Test/FeatureTestTraitTest.php | 18 ++++++++++++++++++ 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 54d89cce6eaf..6f01b96518e6 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -1154,7 +1154,7 @@ protected function outputBufferingEnd(): string $buffer = ''; while (ob_get_level() > $this->bufferLevel) { - $buffer = ob_get_contents(); + $buffer .= ob_get_contents(); ob_end_clean(); } diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index e6e8492ea466..fd12efd4962b 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -144,14 +144,6 @@ public function skipEvents() */ public function call(string $method, string $path, ?array $params = null) { - $buffer = \ob_get_level(); - - // Clean up any open output buffers - // not relevant to unit testing - if (\ob_get_level() > 0 && (! isset($this->clean) || $this->clean === true)) { - \ob_end_clean(); // @codeCoverageIgnore - } - // Simulate having a blank session $_SESSION = []; $_SERVER['REQUEST_METHOD'] = $method; @@ -180,23 +172,9 @@ public function call(string $method, string $path, ?array $params = null) ->setRequest($request) ->run($routes, true); - $output = \ob_get_contents(); - if (empty($response->getBody()) && ! empty($output)) { - $response->setBody($output); - } - // Reset directory if it has been set Services::router()->setDirectory(null); - // Ensure the output buffer is identical so no tests are risky - while (\ob_get_level() > $buffer) { - \ob_end_clean(); // @codeCoverageIgnore - } - - while (\ob_get_level() < $buffer) { - \ob_start(); // @codeCoverageIgnore - } - return new TestResponse($response); } diff --git a/tests/system/Test/FeatureTestTraitTest.php b/tests/system/Test/FeatureTestTraitTest.php index 4636d58d3927..e6c8aef13e2c 100644 --- a/tests/system/Test/FeatureTestTraitTest.php +++ b/tests/system/Test/FeatureTestTraitTest.php @@ -74,6 +74,24 @@ public function testCallSimpleGet() $this->assertSame(200, $response->response()->getStatusCode()); } + public function testClosureWithEcho() + { + $this->withRoutes([ + [ + 'get', + 'home', + static function () { echo 'test echo'; }, + ], + ]); + + $response = $this->get('home'); + $this->assertInstanceOf(TestResponse::class, $response); + $this->assertInstanceOf(Response::class, $response->response()); + $this->assertTrue($response->isOK()); + $this->assertSame('test echo', $response->response()->getBody()); + $this->assertSame(200, $response->response()->getStatusCode()); + } + public function testCallPost() { $this->withRoutes([ From f6d971024f6e96e50c25e800898a4a9254347998 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Mon, 15 May 2023 20:02:27 +0800 Subject: [PATCH 190/485] trigger GitHub actions From f48e408be1220254f906272d05c498e9fe858cb0 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Wed, 17 May 2023 16:28:08 +0700 Subject: [PATCH 191/485] refactor: GDHandler make WebP with option quality --- system/Images/Handlers/GDHandler.php | 2 +- user_guide_src/source/libraries/images.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 6063ef94496b..0647b91adc96 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -267,7 +267,7 @@ public function save(?string $target = null, int $quality = 90): bool throw ImageException::forInvalidImageCreate(lang('Images.webpNotSupported')); } - if (! @imagewebp($this->resource, $target)) { + if (! @imagewebp($this->resource, $target, $quality)) { throw ImageException::forSaveFailed(); } break; diff --git a/user_guide_src/source/libraries/images.rst b/user_guide_src/source/libraries/images.rst index 8c4ed8c7b711..88ed427f7d9c 100644 --- a/user_guide_src/source/libraries/images.rst +++ b/user_guide_src/source/libraries/images.rst @@ -85,7 +85,7 @@ Image Quality ``save()`` can take an additional parameter ``$quality`` to alter the resulting image 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: +only applies to JPEG and WEBP images and will be ignored otherwise: .. literalinclude:: images/005.php From 01d1b4471059f2465d643d77efd841d818898cc3 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Wed, 17 May 2023 16:35:40 +0700 Subject: [PATCH 192/485] docs: correction words --- user_guide_src/source/libraries/images.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/images.rst b/user_guide_src/source/libraries/images.rst index 88ed427f7d9c..e60336e16db7 100644 --- a/user_guide_src/source/libraries/images.rst +++ b/user_guide_src/source/libraries/images.rst @@ -85,7 +85,7 @@ Image Quality ``save()`` can take an additional parameter ``$quality`` to alter the resulting image quality. Values range from 0 to 100 with 90 being the framework default. This parameter -only applies to JPEG and WEBP images and will be ignored otherwise: +only applies to JPEG and WEBP images, will be ignored otherwise: .. literalinclude:: images/005.php From add72ab3659caec77624da184cbae76467cc078a Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Thu, 18 May 2023 08:22:20 +0700 Subject: [PATCH 193/485] docs: added about WebP quality --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + user_guide_src/source/libraries/images.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 652f4f03c579..1b0cfe4fc2b3 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -87,6 +87,7 @@ Libraries - **Validation:** Added ``Validation::getValidated()`` method that gets the actual validated data. See :ref:`validation-getting-validated-data` for details. +- **Images:** Now WebP images can be used option ``$quality`` for compression Helpers and Functions ===================== diff --git a/user_guide_src/source/libraries/images.rst b/user_guide_src/source/libraries/images.rst index e60336e16db7..ae4a8c87c66c 100644 --- a/user_guide_src/source/libraries/images.rst +++ b/user_guide_src/source/libraries/images.rst @@ -87,6 +87,8 @@ Image Quality quality. Values range from 0 to 100 with 90 being the framework default. This parameter only applies to JPEG and WEBP images, will be ignored otherwise: +.. note:: Parameter ``$quality`` for WebP can be used since ``v4.4.0`` + .. literalinclude:: images/005.php .. note:: Higher quality will result in larger file sizes. See also https://www.php.net/manual/en/function.imagejpeg.php From 9ba320fcbe5f0d190d1623223c6cf659363ff2f1 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Thu, 18 May 2023 08:23:19 +0700 Subject: [PATCH 194/485] docs: added about WebP quality --- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 1b0cfe4fc2b3..a79ba4480764 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -87,7 +87,7 @@ Libraries - **Validation:** Added ``Validation::getValidated()`` method that gets the actual validated data. See :ref:`validation-getting-validated-data` for details. -- **Images:** Now WebP images can be used option ``$quality`` for compression +- **Images:** Now WebP images can be used option ``$quality`` for compression. Helpers and Functions ===================== From 3f2d0957f93ea7ec58a231426288aa0648518f30 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 18 May 2023 17:03:58 +0900 Subject: [PATCH 195/485] feat: add Session::close() --- system/Session/Session.php | 14 ++++++++++ user_guide_src/source/libraries/sessions.rst | 26 ++++++++++++++++++- .../source/libraries/sessions/003.php | 2 +- .../source/libraries/sessions/044.php | 3 +++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 user_guide_src/source/libraries/sessions/044.php diff --git a/system/Session/Session.php b/system/Session/Session.php index 3ea5bcd00b53..c68b94df29c1 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -451,6 +451,20 @@ public function destroy() session_destroy(); } + /** + * Writes session data and close the current session. + * + * @return void + */ + public function close() + { + if (ENVIRONMENT === 'testing') { + return; + } + + session_write_close(); + } + /** * Sets user data into the session. * diff --git a/user_guide_src/source/libraries/sessions.rst b/user_guide_src/source/libraries/sessions.rst index 7f316c569276..7dede5a3dbc4 100644 --- a/user_guide_src/source/libraries/sessions.rst +++ b/user_guide_src/source/libraries/sessions.rst @@ -342,6 +342,30 @@ intend to reuse that same key in the same request, you'd want to use .. literalinclude:: sessions/036.php +Closing a Session +================= + +.. _session-close: + +close() +------- + +.. versionadded:: 4.4.0 + +To close the current session manually after you no longer need it, use the +``close()`` method: + +.. literalinclude:: sessions/044.php + +You do not have to close the session manually, PHP will close it automatically +after your script terminated. But as session data is locked to prevent concurrent +writes only one request may operate on a session at any time. You may improve +your site performance by closing the session as soon as all changes to session +data are done. + +This method will work in exactly the same way as PHP's +`session_write_close() `_ function. + Destroying a Session ==================== @@ -529,7 +553,7 @@ DatabaseHandler Driver 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 + support such cases. Use the :ref:`session-close` method after you've done processing session data if you're having performance issues. diff --git a/user_guide_src/source/libraries/sessions/003.php b/user_guide_src/source/libraries/sessions/003.php index ed979ddc0738..13d9b068f8cc 100644 --- a/user_guide_src/source/libraries/sessions/003.php +++ b/user_guide_src/source/libraries/sessions/003.php @@ -1,3 +1,3 @@ close(); diff --git a/user_guide_src/source/libraries/sessions/044.php b/user_guide_src/source/libraries/sessions/044.php new file mode 100644 index 000000000000..13d9b068f8cc --- /dev/null +++ b/user_guide_src/source/libraries/sessions/044.php @@ -0,0 +1,3 @@ +close(); From cf512475be0ee19ab14438f7b135eddc20ed91cf Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Thu, 18 May 2023 16:18:52 +0700 Subject: [PATCH 196/485] docs: correction note --- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- user_guide_src/source/libraries/images.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index a79ba4480764..435022e49bbe 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -87,7 +87,7 @@ Libraries - **Validation:** Added ``Validation::getValidated()`` method that gets the actual validated data. See :ref:`validation-getting-validated-data` for details. -- **Images:** Now WebP images can be used option ``$quality`` for compression. +- **Images:** The option ``$quality`` can now be used to compress WebP images. Helpers and Functions ===================== diff --git a/user_guide_src/source/libraries/images.rst b/user_guide_src/source/libraries/images.rst index ae4a8c87c66c..0507c5d36529 100644 --- a/user_guide_src/source/libraries/images.rst +++ b/user_guide_src/source/libraries/images.rst @@ -87,7 +87,7 @@ Image Quality quality. Values range from 0 to 100 with 90 being the framework default. This parameter only applies to JPEG and WEBP images, will be ignored otherwise: -.. note:: Parameter ``$quality`` for WebP can be used since ``v4.4.0`` +.. note:: The parameter ``$quality`` for WebP can be used since v4.4.0. .. literalinclude:: images/005.php From ff38f5c581a3de79f286e7f74bfb8d5816b44ae5 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Mon, 22 May 2023 11:28:42 +0800 Subject: [PATCH 197/485] Disabling output buffering in the DownloadResponse::send method --- system/HTTP/DownloadResponse.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/system/HTTP/DownloadResponse.php b/system/HTTP/DownloadResponse.php index 19e3c657795e..e83f9453455d 100644 --- a/system/HTTP/DownloadResponse.php +++ b/system/HTTP/DownloadResponse.php @@ -252,6 +252,13 @@ public function setCache(array $options = []) */ public function send() { + // Turn off output buffering completely, even if php.ini output_buffering is not off + if (ENVIRONMENT !== 'testing') { + while (ob_get_level() > 0) { + ob_end_clean(); + } + } + $this->buildHeaders(); $this->sendHeaders(); $this->sendBody(); From 9f67962c05df5683d6b8a31bdac8cbbb6409ee81 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Wed, 24 May 2023 18:00:04 +0800 Subject: [PATCH 198/485] feature: a single point of sending the Response. --- system/CodeIgniter.php | 83 ++++++--------------- tests/system/CodeIgniterTest.php | 7 +- user_guide_src/source/changelogs/v4.4.0.rst | 1 + 3 files changed, 28 insertions(+), 63 deletions(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 68d70d2bc3e2..54e2f70f8872 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -162,6 +162,8 @@ class CodeIgniter /** * Whether to return Response object or send response. + * + * @deprecated No longer used. */ protected bool $returnResponse = false; @@ -321,8 +323,6 @@ private function configureKint(): void */ public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false) { - $this->returnResponse = $returnResponse; - if ($this->context === null) { throw new LogicException( 'Context must be set before run() is called. If you are upgrading from 4.1.x, ' @@ -341,37 +341,8 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon $this->spoofRequestMethod(); - if ($this->request instanceof IncomingRequest && strtolower($this->request->getMethod()) === 'cli') { - $this->response->setStatusCode(405)->setBody('Method Not Allowed'); - - if ($this->returnResponse) { - return $this->response; - } - - $this->sendResponse(); - - return; - } - - Events::trigger('pre_system'); - - // Check for a cached page. Execution will stop - // if the page has been cached. - $cacheConfig = config(Cache::class); - $response = $this->displayCache($cacheConfig); - if ($response instanceof ResponseInterface) { - if ($returnResponse) { - return $response; - } - - $this->response->send(); - $this->callExit(EXIT_SUCCESS); - - return; - } - try { - return $this->handleRequest($routes, $cacheConfig, $returnResponse); + $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse); } catch (RedirectException $e) { $this->outputBufferingEnd(); $logger = Services::logger(); @@ -380,27 +351,20 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon // If the route is a 'redirect' route, it throws // the exception with the $to as the message $this->response->redirect(base_url($e->getMessage()), 'auto', $e->getCode()); - - if ($this->returnResponse) { - return $this->response; - } - - $this->sendResponse(); - - $this->callExit(EXIT_SUCCESS); - - return; } catch (PageNotFoundException $e) { - $return = $this->display404errors($e); - - if ($return instanceof ResponseInterface) { - return $return; - } + $this->response = $this->display404errors($e); } catch (Throwable $e) { $this->outputBufferingEnd(); throw $e; } + + if ($returnResponse) { + return $this->response; + } + + $this->sendResponse(); + $this->callExit(EXIT_SUCCESS); } /** @@ -455,7 +419,17 @@ public function disableFilters(): void */ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cacheConfig, bool $returnResponse = false) { - $this->returnResponse = $returnResponse; + if ($this->request instanceof IncomingRequest && strtolower($this->request->getMethod()) === 'cli') { + return $this->response->setStatusCode(405)->setBody('Method Not Allowed'); + } + + Events::trigger('pre_system'); + + // Check for a cached page. Execution will stop + // if the page has been cached. + if (($response = $this->displayCache($cacheConfig)) instanceof ResponseInterface) { + return $response; + } $routeFilter = $this->tryToRouteIt($routes); @@ -486,7 +460,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache // If a ResponseInterface instance is returned then send it back to the client and stop if ($possibleResponse instanceof ResponseInterface) { - return $this->returnResponse ? $possibleResponse : $possibleResponse->send(); + return $possibleResponse; } if ($possibleResponse instanceof Request) { @@ -561,10 +535,6 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache unset($uri); - if (! $this->returnResponse) { - $this->sendResponse(); - } - // Is there a post-system event? Events::trigger('post_system'); @@ -978,13 +948,8 @@ protected function display404errors(PageNotFoundException $e) $cacheConfig = config(Cache::class); $this->gatherOutput($cacheConfig, $returned); - if ($this->returnResponse) { - return $this->response; - } - - $this->sendResponse(); - return; + return $this->response; } // Display 404 Errors diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index cef482bb4e34..ca084c0ed274 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -142,11 +142,10 @@ public function testRun404OverrideControllerReturnsResponse() $router = Services::router($routes, Services::incomingrequest()); Services::injectMock('router', $router); - ob_start(); - $this->codeigniter->run($routes); - $output = ob_get_clean(); + $response = $this->codeigniter->run($routes, true); - $this->assertStringContainsString('Oops', $output); + $this->assertStringContainsString('Oops', $response->getBody()); + $this->assertSame(567, $response->getStatusCode()); } public function testRun404OverrideReturnResponse() diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index ebfa65ede545..eb98dcfe5e28 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -138,6 +138,7 @@ Deprecations are deprecated. Because these methods have been moved to ``BaseExceptionHandler`` or ``ExceptionHandler``. - **Autoloader:** ``Autoloader::sanitizeFilename()`` is deprecated. +- **CodeIgniter:** ``CodeIgniter::$returnResponse`` property is deprecated. no longer used. Bugs Fixed ********** From 4c928c130bee9b35245b8e1ac3dcf7b7e6bc15dd Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Thu, 25 May 2023 08:21:26 +0700 Subject: [PATCH 199/485] docs: added changes in changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 435022e49bbe..dfd5497f0ee4 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -120,6 +120,7 @@ Message Changes Changes ******* +- **Images:** Since ``v4.4`` quality WebP not using default PHP. - **Config:** The deprecated Cookie items in **app/Config/App.php** has been removed. - **Config:** Routing settings have been moved to **app/Config/Routing.php** config file. See :ref:`Upgrading Guide `. From 964a5044ffe8f5c13ebd6127f89c90fa5601f43d Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean <97607754+ddevsr@users.noreply.github.com> Date: Thu, 25 May 2023 14:15:52 +0700 Subject: [PATCH 200/485] Update user_guide_src/source/changelogs/v4.4.0.rst Co-authored-by: kenjis --- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index dfd5497f0ee4..c0277d6d704b 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -120,7 +120,7 @@ Message Changes Changes ******* -- **Images:** Since ``v4.4`` quality WebP not using default PHP. +- **Images:** The default quality for WebP in ``GDHandler`` has been changed from 80 to 90. - **Config:** The deprecated Cookie items in **app/Config/App.php** has been removed. - **Config:** Routing settings have been moved to **app/Config/Routing.php** config file. See :ref:`Upgrading Guide `. From d14ee1c6fc7546e0e3d5ec8a72d2faba2a4f04d3 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Sun, 4 Jun 2023 11:11:26 +0800 Subject: [PATCH 201/485] Update user_guide_src/source/changelogs/v4.4.0.rst Co-authored-by: kenjis --- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index eb98dcfe5e28..f65565a7792f 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -138,7 +138,7 @@ Deprecations are deprecated. Because these methods have been moved to ``BaseExceptionHandler`` or ``ExceptionHandler``. - **Autoloader:** ``Autoloader::sanitizeFilename()`` is deprecated. -- **CodeIgniter:** ``CodeIgniter::$returnResponse`` property is deprecated. no longer used. +- **CodeIgniter:** ``CodeIgniter::$returnResponse`` property is deprecated. No longer used. Bugs Fixed ********** From fe8808cf7b81abb7fb5359b4024f9e733644ae9f Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Mon, 5 Jun 2023 14:18:22 -0400 Subject: [PATCH 202/485] added `UploadedFile::getClientPath()` Adds access to the file's `full_path` index --- system/HTTP/Files/FileCollection.php | 1 + system/HTTP/Files/UploadedFile.php | 20 ++++++++++++++++++- system/HTTP/Files/UploadedFileInterface.php | 9 ++++++++- .../system/HTTP/Files/FileCollectionTest.php | 12 ++++++----- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/system/HTTP/Files/FileCollection.php b/system/HTTP/Files/FileCollection.php index 87f31ee511ea..66bb46b74b4e 100644 --- a/system/HTTP/Files/FileCollection.php +++ b/system/HTTP/Files/FileCollection.php @@ -180,6 +180,7 @@ protected function createFileObject(array $array) return new UploadedFile( $array['tmp_name'] ?? null, $array['name'] ?? null, + $array['full_path'] ?? null, $array['type'] ?? null, $array['size'] ?? null, $array['error'] ?? null diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index a9e81cd97ee1..757fca872a46 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -34,6 +34,13 @@ class UploadedFile extends File implements UploadedFileInterface */ protected $path; + /** + * The webkit relative path of the file. + * + * @var string + */ + protected $clientPath; + /** * The original filename as provided by the client. * @@ -75,13 +82,15 @@ class UploadedFile extends File implements UploadedFileInterface * * @param string $path The temporary location of the uploaded file. * @param string $originalName The client-provided filename. + * @param string $clientPath The webkit relative path of the uploaded file. * @param string $mimeType The type of file as provided by PHP * @param int $size The size of the file, in bytes * @param int $error The error constant of the upload (one of PHP's UPLOADERRXXX constants) */ - public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $size = null, ?int $error = null) + public function __construct(string $path, string $originalName, ?string $clientPath, ?string $mimeType = null, ?int $size = null, ?int $error = null) { $this->path = $path; + $this->clientPath = $clientPath; $this->name = $originalName; $this->originalName = $originalName; $this->originalMimeType = $mimeType; @@ -267,6 +276,15 @@ public function getClientName(): string return $this->originalName; } + /** + * (PHP 8.1+) + * Returns the webkit relative path of the uploaded file on directory uploads. + */ + public function getClientPath(): string + { + return $this->clientPath; + } + /** * Gets the temporary filename where the file was uploaded to. */ diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php index 37c18554a106..f8b2f0dd0021 100644 --- a/system/HTTP/Files/UploadedFileInterface.php +++ b/system/HTTP/Files/UploadedFileInterface.php @@ -28,11 +28,12 @@ interface UploadedFileInterface * * @param string $path The temporary location of the uploaded file. * @param string $originalName The client-provided filename. + * @param string $clientPath The webkit relative path of the uploaded file. * @param string $mimeType The type of file as provided by PHP * @param int $size The size of the file, in bytes * @param int $error The error constant of the upload (one of PHP's UPLOADERRXXX constants) */ - public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $size = null, ?int $error = null); + public function __construct(string $path, string $originalName, ?string $clientPath, ?string $mimeType = null, ?int $size = null, ?int $error = null); /** * Move the uploaded file to a new location. @@ -109,6 +110,12 @@ public function getName(): string; */ public function getTempName(): string; + /** + * (PHP 8.1+) + * Returns the webkit relative path of the uploaded file on directory uploads. + */ + public function getClientPath(): string; + /** * Returns the original file extension, based on the file name that * was uploaded. This is NOT a trusted source. diff --git a/tests/system/HTTP/Files/FileCollectionTest.php b/tests/system/HTTP/Files/FileCollectionTest.php index c9d2c0123acf..a226297f59bd 100644 --- a/tests/system/HTTP/Files/FileCollectionTest.php +++ b/tests/system/HTTP/Files/FileCollectionTest.php @@ -38,11 +38,12 @@ public function testAllReturnsValidSingleFile() { $_FILES = [ 'userfile' => [ - 'name' => 'someFile.txt', - 'type' => 'text/plain', - 'size' => '124', - 'tmp_name' => '/tmp/myTempFile.txt', - 'error' => 0, + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'full_path' => 'tmp/myTempFile.txt', + 'error' => 0, ], ]; @@ -54,6 +55,7 @@ public function testAllReturnsValidSingleFile() $this->assertInstanceOf(UploadedFile::class, $file); $this->assertSame('someFile.txt', $file->getName()); + $this->assertSame('tmp/myTempFile.txt', $file->getClientPath()); $this->assertSame(124, $file->getSize()); } From 3e2b4c777f52e2ec3b00502275a1ad696e0b94d6 Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Mon, 5 Jun 2023 14:33:54 -0400 Subject: [PATCH 203/485] corrected `UploadedFile::getClientPath()` return types --- system/HTTP/Files/UploadedFile.php | 2 +- system/HTTP/Files/UploadedFileInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index 757fca872a46..7afecf4c3542 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -280,7 +280,7 @@ public function getClientName(): string * (PHP 8.1+) * Returns the webkit relative path of the uploaded file on directory uploads. */ - public function getClientPath(): string + public function getClientPath(): string|null { return $this->clientPath; } diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php index f8b2f0dd0021..8f2d8624f07c 100644 --- a/system/HTTP/Files/UploadedFileInterface.php +++ b/system/HTTP/Files/UploadedFileInterface.php @@ -114,7 +114,7 @@ public function getTempName(): string; * (PHP 8.1+) * Returns the webkit relative path of the uploaded file on directory uploads. */ - public function getClientPath(): string; + public function getClientPath(): string|null; /** * Returns the original file extension, based on the file name that From 8d6d9ad7648ece2dbc9d2f9e2880877589cde46a Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Mon, 5 Jun 2023 15:39:25 -0400 Subject: [PATCH 204/485] added docs for `UploadedFile::getClientPath()` --- user_guide_src/source/libraries/uploaded_files.rst | 8 ++++++++ user_guide_src/source/libraries/uploaded_files/023.php | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 user_guide_src/source/libraries/uploaded_files/023.php diff --git a/user_guide_src/source/libraries/uploaded_files.rst b/user_guide_src/source/libraries/uploaded_files.rst index b91b34ecd718..13b9f9f12aca 100644 --- a/user_guide_src/source/libraries/uploaded_files.rst +++ b/user_guide_src/source/libraries/uploaded_files.rst @@ -304,6 +304,14 @@ version, use ``getMimeType()`` instead: .. literalinclude:: uploaded_files/015.php +getClientPath() +--------------- + +Returns the `webkit relative path `_ of the uploaded file when the client has uploaded files via directory upload. +In PHP versions below 8.1, this returns ``null`` + +.. literalinclude:: uploaded_files/023.php + Moving Files ============ diff --git a/user_guide_src/source/libraries/uploaded_files/023.php b/user_guide_src/source/libraries/uploaded_files/023.php new file mode 100644 index 000000000000..f8accc9087b1 --- /dev/null +++ b/user_guide_src/source/libraries/uploaded_files/023.php @@ -0,0 +1,4 @@ +getClientPath(); +echo $clientPath; // dir/file.txt, or dir/sub_dir/file.txt From 252ef6cdb5c34be6b2f517db782489496d702fc2 Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Mon, 5 Jun 2023 21:39:51 -0400 Subject: [PATCH 205/485] set `$clientPath` default to `null` --- system/HTTP/Files/UploadedFile.php | 2 +- system/HTTP/Files/UploadedFileInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index 7afecf4c3542..d19c905cd8e9 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -87,7 +87,7 @@ class UploadedFile extends File implements UploadedFileInterface * @param int $size The size of the file, in bytes * @param int $error The error constant of the upload (one of PHP's UPLOADERRXXX constants) */ - public function __construct(string $path, string $originalName, ?string $clientPath, ?string $mimeType = null, ?int $size = null, ?int $error = null) + public function __construct(string $path, string $originalName, ?string $clientPath = null, ?string $mimeType = null, ?int $size = null, ?int $error = null) { $this->path = $path; $this->clientPath = $clientPath; diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php index 8f2d8624f07c..2b282069d562 100644 --- a/system/HTTP/Files/UploadedFileInterface.php +++ b/system/HTTP/Files/UploadedFileInterface.php @@ -33,7 +33,7 @@ interface UploadedFileInterface * @param int $size The size of the file, in bytes * @param int $error The error constant of the upload (one of PHP's UPLOADERRXXX constants) */ - public function __construct(string $path, string $originalName, ?string $clientPath, ?string $mimeType = null, ?int $size = null, ?int $error = null); + public function __construct(string $path, string $originalName, ?string $clientPath = null, ?string $mimeType = null, ?int $size = null, ?int $error = null); /** * Move the uploaded file to a new location. From e57bd28818690e3d6297ba393891c381ef34aaba Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Mon, 5 Jun 2023 21:43:29 -0400 Subject: [PATCH 206/485] update `$clientPath` param to last param position --- system/HTTP/Files/FileCollection.php | 4 ++-- system/HTTP/Files/UploadedFile.php | 2 +- system/HTTP/Files/UploadedFileInterface.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/system/HTTP/Files/FileCollection.php b/system/HTTP/Files/FileCollection.php index 66bb46b74b4e..7b117f4f8e02 100644 --- a/system/HTTP/Files/FileCollection.php +++ b/system/HTTP/Files/FileCollection.php @@ -180,10 +180,10 @@ protected function createFileObject(array $array) return new UploadedFile( $array['tmp_name'] ?? null, $array['name'] ?? null, - $array['full_path'] ?? null, $array['type'] ?? null, $array['size'] ?? null, - $array['error'] ?? null + $array['error'] ?? null, + $array['full_path'] ?? null ); } diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index d19c905cd8e9..f5b4667a9cf5 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -87,7 +87,7 @@ class UploadedFile extends File implements UploadedFileInterface * @param int $size The size of the file, in bytes * @param int $error The error constant of the upload (one of PHP's UPLOADERRXXX constants) */ - public function __construct(string $path, string $originalName, ?string $clientPath = null, ?string $mimeType = null, ?int $size = null, ?int $error = null) + public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $size = null, ?int $error = null, ?string $clientPath = null) { $this->path = $path; $this->clientPath = $clientPath; diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php index 2b282069d562..825fc1f4c61f 100644 --- a/system/HTTP/Files/UploadedFileInterface.php +++ b/system/HTTP/Files/UploadedFileInterface.php @@ -33,7 +33,7 @@ interface UploadedFileInterface * @param int $size The size of the file, in bytes * @param int $error The error constant of the upload (one of PHP's UPLOADERRXXX constants) */ - public function __construct(string $path, string $originalName, ?string $clientPath = null, ?string $mimeType = null, ?int $size = null, ?int $error = null); + public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $size = null, ?int $error = null, ?string $clientPath = null); /** * Move the uploaded file to a new location. From 1e5db7600389a65997297fecc9682a64588a0654 Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Mon, 5 Jun 2023 21:55:59 -0400 Subject: [PATCH 207/485] made test full path more realistic --- tests/system/HTTP/Files/FileCollectionTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/HTTP/Files/FileCollectionTest.php b/tests/system/HTTP/Files/FileCollectionTest.php index a226297f59bd..d79032e26b5e 100644 --- a/tests/system/HTTP/Files/FileCollectionTest.php +++ b/tests/system/HTTP/Files/FileCollectionTest.php @@ -42,7 +42,7 @@ public function testAllReturnsValidSingleFile() 'type' => 'text/plain', 'size' => '124', 'tmp_name' => '/tmp/myTempFile.txt', - 'full_path' => 'tmp/myTempFile.txt', + 'full_path' => 'someDir/someFile.txt', 'error' => 0, ], ]; @@ -55,7 +55,7 @@ public function testAllReturnsValidSingleFile() $this->assertInstanceOf(UploadedFile::class, $file); $this->assertSame('someFile.txt', $file->getName()); - $this->assertSame('tmp/myTempFile.txt', $file->getClientPath()); + $this->assertSame('someDir/someFile.txt', $file->getClientPath()); $this->assertSame(124, $file->getSize()); } From 9be23aa5f04c79d484b7835a489238b64c110325 Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Mon, 5 Jun 2023 21:57:38 -0400 Subject: [PATCH 208/485] made return type for `UploadedFile::getClientPath()` nullable --- system/HTTP/Files/UploadedFile.php | 2 +- system/HTTP/Files/UploadedFileInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index f5b4667a9cf5..acd5ca48397f 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -280,7 +280,7 @@ public function getClientName(): string * (PHP 8.1+) * Returns the webkit relative path of the uploaded file on directory uploads. */ - public function getClientPath(): string|null + public function getClientPath(): ?string { return $this->clientPath; } diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php index 825fc1f4c61f..9add0f2f6229 100644 --- a/system/HTTP/Files/UploadedFileInterface.php +++ b/system/HTTP/Files/UploadedFileInterface.php @@ -114,7 +114,7 @@ public function getTempName(): string; * (PHP 8.1+) * Returns the webkit relative path of the uploaded file on directory uploads. */ - public function getClientPath(): string|null; + public function getClientPath(): ?string; /** * Returns the original file extension, based on the file name that From a387052a35b7312ddc092a0945673ce6d1ad7d2c Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Mon, 5 Jun 2023 22:00:11 -0400 Subject: [PATCH 209/485] updated order of `$clientPath` param in doc string --- system/HTTP/Files/UploadedFile.php | 2 +- system/HTTP/Files/UploadedFileInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index acd5ca48397f..44c45571187a 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -82,10 +82,10 @@ class UploadedFile extends File implements UploadedFileInterface * * @param string $path The temporary location of the uploaded file. * @param string $originalName The client-provided filename. - * @param string $clientPath The webkit relative path of the uploaded file. * @param string $mimeType The type of file as provided by PHP * @param int $size The size of the file, in bytes * @param int $error The error constant of the upload (one of PHP's UPLOADERRXXX constants) + * @param string $clientPath The webkit relative path of the uploaded file. */ public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $size = null, ?int $error = null, ?string $clientPath = null) { diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php index 9add0f2f6229..075c4190a381 100644 --- a/system/HTTP/Files/UploadedFileInterface.php +++ b/system/HTTP/Files/UploadedFileInterface.php @@ -28,10 +28,10 @@ interface UploadedFileInterface * * @param string $path The temporary location of the uploaded file. * @param string $originalName The client-provided filename. - * @param string $clientPath The webkit relative path of the uploaded file. * @param string $mimeType The type of file as provided by PHP * @param int $size The size of the file, in bytes * @param int $error The error constant of the upload (one of PHP's UPLOADERRXXX constants) + * @param string $clientPath The webkit relative path of the uploaded file. */ public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $size = null, ?int $error = null, ?string $clientPath = null); From b7d4c2b60ccdea4f33aee764e7393f16ac00a435 Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Mon, 5 Jun 2023 22:05:29 -0400 Subject: [PATCH 210/485] test if `UploadedFile::clientPath()` returns `null` on versions below 8.1 --- tests/system/HTTP/Files/FileCollectionTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/system/HTTP/Files/FileCollectionTest.php b/tests/system/HTTP/Files/FileCollectionTest.php index d79032e26b5e..576227ab72e4 100644 --- a/tests/system/HTTP/Files/FileCollectionTest.php +++ b/tests/system/HTTP/Files/FileCollectionTest.php @@ -55,8 +55,13 @@ public function testAllReturnsValidSingleFile() $this->assertInstanceOf(UploadedFile::class, $file); $this->assertSame('someFile.txt', $file->getName()); - $this->assertSame('someDir/someFile.txt', $file->getClientPath()); $this->assertSame(124, $file->getSize()); + + if (version_compare(PHP_VERSION, '8.1', '>=')) { + $this->assertSame('someDir/someFile.txt', $file->getClientPath()); + } else { + $this->assertNull($file->getClientPath()); + } } public function testAllReturnsValidMultipleFilesSameName() From 43fc0e00d5986d4634ce5085c43a891fcb13dd02 Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Mon, 5 Jun 2023 22:34:09 -0400 Subject: [PATCH 211/485] added proper tests for `UploadedFile::getClientPath()` --- .../system/HTTP/Files/FileCollectionTest.php | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/tests/system/HTTP/Files/FileCollectionTest.php b/tests/system/HTTP/Files/FileCollectionTest.php index 576227ab72e4..7c087e07d18a 100644 --- a/tests/system/HTTP/Files/FileCollectionTest.php +++ b/tests/system/HTTP/Files/FileCollectionTest.php @@ -38,12 +38,11 @@ public function testAllReturnsValidSingleFile() { $_FILES = [ 'userfile' => [ - 'name' => 'someFile.txt', - 'type' => 'text/plain', - 'size' => '124', - 'tmp_name' => '/tmp/myTempFile.txt', - 'full_path' => 'someDir/someFile.txt', - 'error' => 0, + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'error' => 0, ], ]; @@ -56,12 +55,6 @@ public function testAllReturnsValidSingleFile() $this->assertSame('someFile.txt', $file->getName()); $this->assertSame(124, $file->getSize()); - - if (version_compare(PHP_VERSION, '8.1', '>=')) { - $this->assertSame('someDir/someFile.txt', $file->getClientPath()); - } else { - $this->assertNull($file->getClientPath()); - } } public function testAllReturnsValidMultipleFilesSameName() @@ -460,6 +453,41 @@ public function testErrorWithNoError() $this->assertSame(UPLOAD_ERR_OK, $file->getError()); } + public function testClientPathReturnsValidFullPath() + { + $_FILES = [ + 'userfile' => [ + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'full_path' => 'someDir/someFile.txt', + ], + ]; + + $collection = new FileCollection(); + $file = $collection->getFile('userfile'); + + $this->assertSame('someDir/someFile.txt', $file->getClientPath()); + } + + public function testClientPathReturnsNullWhenFullPathIsNull() + { + $_FILES = [ + 'userfile' => [ + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + ], + ]; + + $collection = new FileCollection(); + $file = $collection->getFile('userfile'); + + $this->assertNull($file->getClientPath()); + } + public function testFileReturnsValidSingleFile() { $_FILES = [ From 6dbfa76387b5eec6b15667383e9a281d70f60eb0 Mon Sep 17 00:00:00 2001 From: "T.R.M. Batt" <73239367+JamminCoder@users.noreply.github.com> Date: Mon, 5 Jun 2023 22:37:34 -0400 Subject: [PATCH 212/485] add `version added` to new method Co-authored-by: kenjis --- user_guide_src/source/libraries/uploaded_files.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/libraries/uploaded_files.rst b/user_guide_src/source/libraries/uploaded_files.rst index 13b9f9f12aca..6a981100696b 100644 --- a/user_guide_src/source/libraries/uploaded_files.rst +++ b/user_guide_src/source/libraries/uploaded_files.rst @@ -307,6 +307,8 @@ version, use ``getMimeType()`` instead: getClientPath() --------------- +.. versionadded:: 4.4.0 + Returns the `webkit relative path `_ of the uploaded file when the client has uploaded files via directory upload. In PHP versions below 8.1, this returns ``null`` From 5e86bd1a73f41ee52bf3518d3c573275d48e57dd Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Mon, 5 Jun 2023 22:45:14 -0400 Subject: [PATCH 213/485] added change description for `UploadedFile` --- user_guide_src/source/changelogs/v4.4.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index f0d803b7e36e..c79e9fb56571 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -89,6 +89,9 @@ Libraries the actual validated data. See :ref:`validation-getting-validated-data` for details. - **Images:** The option ``$quality`` can now be used to compress WebP images. +- **Uploaded Files:** Added ``UploadedFiles::getClientPath()`` method that returns + the value of the `full_path` index of the file if it was uploaded via directory upload. + Helpers and Functions ===================== From 9d32e460156742f4a0769aa7b2feff646db713d1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Jun 2023 14:55:35 +0900 Subject: [PATCH 214/485] refactor: fix incorrect return value See https://www.php.net/manual/en/language.oop5.overloading.php#language.oop5.overloading.members --- system/Entity/Entity.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 8495fadf6126..35e10414d1f8 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -465,7 +465,7 @@ public function __set(string $key, $value = null) if (method_exists($this, '_' . $method)) { $this->{'_' . $method}($value); - return $this; + return; } // If a "`set` + $key" method exists, it is also a setter. From b9f5c83593598b3a1296ad9d2631e5044c5d0c47 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Wed, 7 Jun 2023 01:42:58 +0800 Subject: [PATCH 215/485] refactor: moving RedirectException. --- system/CodeIgniter.php | 2 +- .../Utilities/Routes/FilterFinder.php | 2 +- system/HTTP/Exceptions/RedirectException.php | 28 +++++++++++++++++++ .../Router/Exceptions/RedirectException.php | 2 ++ system/Router/Router.php | 2 +- system/Test/FeatureTestCase.php | 3 +- system/Test/FeatureTestTrait.php | 3 +- tests/system/Router/RouterTest.php | 2 +- user_guide_src/source/changelogs/v4.4.0.rst | 1 + 9 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 system/HTTP/Exceptions/RedirectException.php diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 54e2f70f8872..2d919d3394cf 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -18,12 +18,12 @@ use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\DownloadResponse; +use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\URI; -use CodeIgniter\Router\Exceptions\RedirectException; use CodeIgniter\Router\RouteCollectionInterface; use CodeIgniter\Router\Router; use Config\App; diff --git a/system/Commands/Utilities/Routes/FilterFinder.php b/system/Commands/Utilities/Routes/FilterFinder.php index 0f25eebf47f1..59e4ac49fbee 100644 --- a/system/Commands/Utilities/Routes/FilterFinder.php +++ b/system/Commands/Utilities/Routes/FilterFinder.php @@ -13,7 +13,7 @@ use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Filters\Filters; -use CodeIgniter\Router\Exceptions\RedirectException; +use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\Router\Router; use Config\Services; diff --git a/system/HTTP/Exceptions/RedirectException.php b/system/HTTP/Exceptions/RedirectException.php new file mode 100644 index 000000000000..5cda9fb00cd0 --- /dev/null +++ b/system/HTTP/Exceptions/RedirectException.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP\Exceptions; + +use CodeIgniter\Exceptions\HTTPExceptionInterface; +use Exception; + +/** + * RedirectException + */ +class RedirectException extends Exception implements HTTPExceptionInterface +{ + /** + * HTTP status code for redirects + * + * @var int + */ + protected $code = 302; +} diff --git a/system/Router/Exceptions/RedirectException.php b/system/Router/Exceptions/RedirectException.php index a23d651855de..5e19e2e9ac5c 100644 --- a/system/Router/Exceptions/RedirectException.php +++ b/system/Router/Exceptions/RedirectException.php @@ -16,6 +16,8 @@ /** * RedirectException + * + * @deprecated Use \CodeIgniter\HTTP\Exceptions\RedirectException instead */ class RedirectException extends Exception implements HTTPExceptionInterface { diff --git a/system/Router/Router.php b/system/Router/Router.php index 8dd35c656e39..1ec3e6aa36d3 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -13,8 +13,8 @@ use Closure; use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\Request; -use CodeIgniter\Router\Exceptions\RedirectException; use CodeIgniter\Router\Exceptions\RouterException; /** diff --git a/system/Test/FeatureTestCase.php b/system/Test/FeatureTestCase.php index 2c2233df81b0..7119b914493b 100644 --- a/system/Test/FeatureTestCase.php +++ b/system/Test/FeatureTestCase.php @@ -12,12 +12,11 @@ namespace CodeIgniter\Test; use CodeIgniter\Events\Events; +use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\URI; use CodeIgniter\HTTP\UserAgent; -use CodeIgniter\Router\Exceptions\RedirectException; -use CodeIgniter\Router\RouteCollection; use Config\Services; use Exception; use ReflectionException; diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index fd12efd4962b..30d6f23b2bf1 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -12,11 +12,10 @@ namespace CodeIgniter\Test; use CodeIgniter\Events\Events; +use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\URI; -use CodeIgniter\Router\Exceptions\RedirectException; -use CodeIgniter\Router\RouteCollection; use Config\App; use Config\Services; use Exception; diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php index 2898f154716e..bf23289ba191 100644 --- a/tests/system/Router/RouterTest.php +++ b/tests/system/Router/RouterTest.php @@ -13,8 +13,8 @@ use CodeIgniter\Config\Services; use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; -use CodeIgniter\Router\Exceptions\RedirectException; use CodeIgniter\Router\Exceptions\RouterException; use CodeIgniter\Test\CIUnitTestCase; use Config\Modules; diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index aab872fa6c1c..af154ec748be 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -143,6 +143,7 @@ Deprecations ``ExceptionHandler``. - **Autoloader:** ``Autoloader::sanitizeFilename()`` is deprecated. - **CodeIgniter:** ``CodeIgniter::$returnResponse`` property is deprecated. No longer used. +- **RedirectException:** ``\CodeIgniter\Router\Exceptions\RedirectException`` is deprecated. Use \CodeIgniter\HTTP\Exceptions\RedirectException instead. Bugs Fixed ********** From 6cffb8764ffd530597b15f83edd5fa5eb19eebfc Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Wed, 7 Jun 2023 12:34:10 +0800 Subject: [PATCH 216/485] update user guide --- user_guide_src/source/general/errors/010.php | 2 +- user_guide_src/source/general/errors/011.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/general/errors/010.php b/user_guide_src/source/general/errors/010.php index ff9cfee974ef..9bae2ed6354b 100644 --- a/user_guide_src/source/general/errors/010.php +++ b/user_guide_src/source/general/errors/010.php @@ -1,3 +1,3 @@ Date: Wed, 7 Jun 2023 23:35:14 +0800 Subject: [PATCH 217/485] Test and exception handler for backwards compatibility. --- system/CodeIgniter.php | 3 ++- tests/system/CodeIgniterTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 2d919d3394cf..b2b0a19bddf6 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -24,6 +24,7 @@ use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\URI; +use CodeIgniter\Router\Exceptions\RedirectException as DeprecatedRedirectException; use CodeIgniter\Router\RouteCollectionInterface; use CodeIgniter\Router\Router; use Config\App; @@ -343,7 +344,7 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon try { $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse); - } catch (RedirectException $e) { + } catch (RedirectException|DeprecatedRedirectException $e) { $this->outputBufferingEnd(); $logger = Services::logger(); $logger->info('REDIRECTED ROUTE at ' . $e->getMessage()); diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index ca084c0ed274..8f45881ba944 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -14,6 +14,7 @@ use CodeIgniter\Config\Services; use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\HTTP\Response; +use CodeIgniter\Router\Exceptions\RedirectException; use CodeIgniter\Router\RouteCollection; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Filters\CITestStreamFilter; @@ -570,6 +571,29 @@ public function testRunRedirectionWithPOSTAndHTTPCode301() $this->assertSame(301, $response->getStatusCode()); } + /** + * test for deprecated \CodeIgniter\Router\Exceptions\RedirectException for backward compatibility + */ + public function testRedirectExceptionDeprecated(): void + { + $_SERVER['argv'] = ['index.php', '/']; + $_SERVER['argc'] = 2; + + // Inject mock router. + $routes = Services::routes(); + $routes->get('/', static function () { + throw new RedirectException('redirect-exception', 503); + }); + + $router = Services::router($routes, Services::incomingrequest()); + Services::injectMock('router', $router); + + $response = $this->codeigniter->run($routes, true); + + $this->assertSame(503, $response->getStatusCode()); + $this->assertSame('http://example.com/redirect-exception', $response->getHeaderLine('Location')); + } + public function testStoresPreviousURL() { $_SERVER['argv'] = ['index.php', '/']; From 0b5965496e56d21ab68c147df43763a05bff2ff2 Mon Sep 17 00:00:00 2001 From: Timothy Batt Date: Thu, 8 Jun 2023 10:17:08 -0400 Subject: [PATCH 218/485] order variable assingment to match parameter order --- system/HTTP/Files/UploadedFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index 44c45571187a..70ece748d834 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -90,12 +90,12 @@ class UploadedFile extends File implements UploadedFileInterface public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $size = null, ?int $error = null, ?string $clientPath = null) { $this->path = $path; - $this->clientPath = $clientPath; $this->name = $originalName; $this->originalName = $originalName; $this->originalMimeType = $mimeType; $this->size = $size; $this->error = $error; + $this->clientPath = $clientPath; parent::__construct($path, false); } From 485da0379afb146d25e54e5172bd8825d3a95fe9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 14 Jun 2023 08:53:40 +0900 Subject: [PATCH 219/485] fix: merge conflicts https://github.com/codeigniter4/CodeIgniter4/pull/7543 --- system/Router/AutoRouterImproved.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 4f88aac942c5..e36b84b61c3d 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -254,9 +254,9 @@ public function getRoute(string $uri, string $httpVerb): array 'Cannot access the default method "' . $this->method . '" with the method name URI path.' ); } - } elseif (method_exists($this->controller, $this->defaultMethod)) { + } elseif (method_exists($this->controller, $defaultMethod)) { // The default method is found. - $this->method = $this->defaultMethod; + $this->method = $defaultMethod; } else { // No method is found. throw PageNotFoundException::forControllerNotFound($this->controller, $method); From 54f2f65300ff36ca0481422ed8a8b55cd5d2236c Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 14 Jun 2023 08:54:38 +0900 Subject: [PATCH 220/485] test: update out-of-dated test code --- tests/system/Router/AutoRouterImprovedTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index 5dc61c992d23..affc6a021f51 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -80,7 +80,7 @@ public function testAutoRouteFindsModuleDefaultControllerAndMethodGet() $router = $this->createNewAutoRouter('get', 'App/Controllers'); [$directory, $controller, $method, $params] - = $router->getRoute('test'); + = $router->getRoute('test', 'get'); $this->assertNull($directory); $this->assertSame('\\' . Index::class, $controller); @@ -172,7 +172,7 @@ public function testAutoRouteFindsControllerWithSubSubfolder() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('subfolder/sub/mycontroller/somemethod'); + = $router->getRoute('subfolder/sub/mycontroller/somemethod', 'get'); $this->assertSame('Subfolder/Sub/', $directory); $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Sub\Mycontroller::class, $controller); @@ -240,7 +240,7 @@ public function testAutoRouteFallbackToDefaultMethod() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('index/15'); + = $router->getRoute('index/15', 'get'); $this->assertNull($directory); $this->assertSame('\\' . Index::class, $controller); @@ -253,7 +253,7 @@ public function testAutoRouteFallbackToDefaultControllerOneParam() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('subfolder/15'); + = $router->getRoute('subfolder/15', 'get'); $this->assertSame('Subfolder/', $directory); $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); @@ -266,7 +266,7 @@ public function testAutoRouteFallbackToDefaultControllerTwoParams() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('subfolder/15/20'); + = $router->getRoute('subfolder/15/20', 'get'); $this->assertSame('Subfolder/', $directory); $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); @@ -279,7 +279,7 @@ public function testAutoRouteFallbackToDefaultControllerNoParams() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('subfolder'); + = $router->getRoute('subfolder', 'get'); $this->assertSame('Subfolder/', $directory); $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); From 2bb2cd39846a137ee68474438cbff1f36ae70d4a Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 10:00:17 +0900 Subject: [PATCH 221/485] refactor: rename parameter name --- system/Router/AutoRouterImproved.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index e36b84b61c3d..42935f11b520 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -373,10 +373,10 @@ private function isValidSegment(string $segment): bool return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment); } - private function translateURIDashes(string $classname): string + private function translateURIDashes(string $segment): string { return $this->translateURIDashes - ? str_replace('-', '_', $classname) - : $classname; + ? str_replace('-', '_', $segment) + : $segment; } } From e7cf718a6530a3e890ec0b00e93744d4a8830ac6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 10:15:29 +0900 Subject: [PATCH 222/485] refactor: add property $segments --- system/Router/AutoRouterImproved.php | 29 ++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 42935f11b520..e3460527e69c 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -70,6 +70,11 @@ final class AutoRouterImproved implements AutoRouterInterface */ private string $defaultMethod; + /** + * The URI segments. + */ + private array $segments = []; + /** * @param class-string[] $protectedControllers * @param string $defaultController Short classname @@ -108,12 +113,12 @@ private function createSegments(string $uri) * If there is a controller corresponding to the first segment, the search * ends there. The remaining segments are parameters to the controller. * - * @param array $segments URI segments - * * @return bool true if a controller class is found. */ - private function searchFirstController(array $segments): bool + private function searchFirstController(): bool { + $segments = $this->segments; + $controller = '\\' . $this->namespace; while ($segments !== []) { @@ -142,12 +147,12 @@ private function searchFirstController(array $segments): bool /** * Search for the last default controller corresponding to the URI segments. * - * @param array $segments URI segments - * * @return bool true if a controller class is found. */ - private function searchLastDefaultController(array $segments): bool + private function searchLastDefaultController(): bool { + $segments = $this->segments; + $params = []; while ($segments !== []) { @@ -195,19 +200,19 @@ public function getRoute(string $uri, string $httpVerb): array $defaultMethod = strtolower($httpVerb) . ucfirst($this->defaultMethod); $this->method = $defaultMethod; - $segments = $this->createSegments($uri); + $this->segments = $this->createSegments($uri); // Check for Module Routes. if ( - $segments !== [] + $this->segments !== [] && ($routingConfig = config(Routing::class)) - && array_key_exists($segments[0], $routingConfig->moduleRoutes) + && array_key_exists($this->segments[0], $routingConfig->moduleRoutes) ) { - $uriSegment = array_shift($segments); + $uriSegment = array_shift($this->segments); $this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\'); } - if ($this->searchFirstController($segments)) { + if ($this->searchFirstController()) { // Controller is found. $baseControllerName = class_basename($this->controller); @@ -219,7 +224,7 @@ public function getRoute(string $uri, string $httpVerb): array 'Cannot access the default controller "' . $this->controller . '" with the controller name URI path.' ); } - } elseif ($this->searchLastDefaultController($segments)) { + } elseif ($this->searchLastDefaultController()) { // The default Controller is found. $baseControllerName = class_basename($this->controller); } else { From 0070c4aa5d65bedc6dac0f3d93f96f0dd8ef5185 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 10:59:29 +0900 Subject: [PATCH 223/485] feat: add properties for positions in the URI segments --- system/Router/AutoRouterImproved.php | 71 ++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index e3460527e69c..6cefbc03e6c8 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -75,6 +75,24 @@ final class AutoRouterImproved implements AutoRouterInterface */ private array $segments = []; + /** + * The position of the Controller in the URI segments. + * Null for the default controller. + */ + private ?int $controllerPos = null; + + /** + * The position of the Method in the URI segments. + * Null for the default method. + */ + private ?int $methodPos = null; + + /** + * The position of the first Parameter in the URI segments. + * Null for the no parameters. + */ + private ?int $paramPos = null; + /** * @param class-string[] $protectedControllers * @param string $defaultController Short classname @@ -121,9 +139,13 @@ private function searchFirstController(): bool $controller = '\\' . $this->namespace; + $controllerPos = -1; + while ($segments !== []) { $segment = array_shift($segments); - $class = $this->translateURIDashes(ucfirst($segment)); + $controllerPos++; + + $class = $this->translateURIDashes(ucfirst($segment)); // as soon as we encounter any segment that is not PSR-4 compliant, stop searching if (! $this->isValidSegment($class)) { @@ -133,9 +155,14 @@ private function searchFirstController(): bool $controller .= '\\' . $class; if (class_exists($controller)) { - $this->controller = $controller; + $this->controller = $controller; + $this->controllerPos = $controllerPos; + // The first item may be a method name. $this->params = $segments; + if ($segments !== []) { + $this->paramPos = $this->controllerPos + 1; + } return true; } @@ -153,9 +180,15 @@ private function searchLastDefaultController(): bool { $segments = $this->segments; - $params = []; + $segmentCount = count($this->segments); + $paramPos = null; + $params = []; while ($segments !== []) { + if ($segmentCount > count($segments)) { + $paramPos = count($segments); + } + $namespaces = array_map( fn ($segment) => $this->translateURIDashes(ucfirst($segment)), $segments @@ -169,6 +202,10 @@ private function searchLastDefaultController(): bool $this->controller = $controller; $this->params = $params; + if ($params !== []) { // @phpstan-ignore-line + $this->paramPos = $paramPos; + } + return true; } @@ -184,6 +221,10 @@ private function searchLastDefaultController(): bool $this->controller = $controller; $this->params = $params; + if ($params !== []) { // @phpstan-ignore-line + $this->paramPos = 0; + } + return true; } @@ -232,6 +273,7 @@ public function getRoute(string $uri, string $httpVerb): array throw new PageNotFoundException('No controller is found for: ' . $uri); } + // The first item may be a method name. $params = $this->params; $methodParam = array_shift($params); @@ -246,6 +288,15 @@ public function getRoute(string $uri, string $httpVerb): array $this->method = $method; $this->params = $params; + // Update the positions. + $this->methodPos = $this->paramPos; + if ($params === []) { + $this->paramPos = null; + } + if ($this->paramPos !== null) { + $this->paramPos++; + } + // Prevent access to default controller's method if (strtolower($baseControllerName) === strtolower($this->defaultController)) { throw new PageNotFoundException( @@ -285,6 +336,20 @@ public function getRoute(string $uri, string $httpVerb): array return [$this->directory, $this->controller, $this->method, $this->params]; } + /** + * @internal For test purpose only. + * + * @return array + */ + public function getPos(): array + { + return [ + 'controller' => $this->controllerPos, + 'method' => $this->methodPos, + 'params' => $this->paramPos, + ]; + } + /** * Get the directory path from the controller and set it to the property. * From e49cf115c4b9b1163b49dcf550b5622cd9402bd2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 11:31:09 +0900 Subject: [PATCH 224/485] fix: add check for underscores in URI when $translateURIDashes is true When converting dashes to underscores, two URIs correspond to a single controller, one URI for dashes and one URI for underscores. This was incorrect behavior, contrary to the design philosophy. --- system/Router/AutoRouterImproved.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 6cefbc03e6c8..6ba9f35b5576 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -324,6 +324,10 @@ public function getRoute(string $uri, string $httpVerb): array // Ensure the controller does not have _remap() method. $this->checkRemap(); + // Ensure the URI segments for the controller and method do not contain + // underscores when $translateURIDashes is true. + $this->checkUnderscore($uri); + // Check parameter count try { $this->checkParameters($uri); @@ -433,6 +437,28 @@ private function checkRemap(): void } } + private function checkUnderscore(string $uri): void + { + if ($this->translateURIDashes === false) { + return; + } + + $paramPos = $this->paramPos ?? count($this->segments); + + for ($i = 0; $i < $paramPos; $i++) { + if (strpos($this->segments[$i], '_') !== false) { + throw new PageNotFoundException( + 'AutoRouterImproved prohibits access to the URI' + . ' containing underscores ("' . $this->segments[$i] . '")' + . ' when $translateURIDashes is enabled.' + . ' Please use the dash.' + . ' Handler:' . $this->controller . '::' . $this->method + . ', URI:' . $uri + ); + } + } + } + /** * Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment * From cde38898e6b2f618aa8777c5050516e131f3ddc0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 11:56:37 +0900 Subject: [PATCH 225/485] test: add tests --- .../system/Router/AutoRouterImprovedTest.php | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index affc6a021f51..4ea843a52d62 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -352,4 +352,66 @@ public function testRejectsControllerWithRemapMethod() $router->getRoute('remap/test', 'get'); } + + public function testRejectsURIWithUnderscoreFolder() + { + $this->expectException(PageNotFoundException::class); + $this->expectExceptionMessage( + 'AutoRouterImproved prohibits access to the URI containing underscores ("dash_folder")' + ); + + $router = $this->createNewAutoRouter(); + + $router->getRoute('dash_folder'); + } + + public function testRejectsURIWithUnderscoreController() + { + $this->expectException(PageNotFoundException::class); + $this->expectExceptionMessage( + 'AutoRouterImproved prohibits access to the URI containing underscores ("dash_controller")' + ); + + $router = $this->createNewAutoRouter(); + + $router->getRoute('dash-folder/dash_controller/dash-method'); + } + + public function testRejectsURIWithUnderscoreMethod() + { + $this->expectException(PageNotFoundException::class); + $this->expectExceptionMessage( + 'AutoRouterImproved prohibits access to the URI containing underscores ("dash_method")' + ); + + $router = $this->createNewAutoRouter(); + + $router->getRoute('dash-folder/dash-controller/dash_method'); + } + + public function testPermitsURIWithUnderscoreParam() + { + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute('mycontroller/somemethod/a_b'); + + $this->assertNull($directory); + $this->assertSame('\\' . Mycontroller::class, $controller); + $this->assertSame('getSomemethod', $method); + $this->assertSame(['a_b'], $params); + } + + public function testDoesNotTranslateDashInParam() + { + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute('mycontroller/somemethod/a-b'); + + $this->assertNull($directory); + $this->assertSame('\\' . Mycontroller::class, $controller); + $this->assertSame('getSomemethod', $method); + $this->assertSame(['a-b'], $params); + } } From 9be93dd45960e4e24904d2b9c27cce05d53468ba Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 13:06:11 +0900 Subject: [PATCH 226/485] test: add tests for getPos() --- .../system/Router/AutoRouterImprovedTest.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index 4ea843a52d62..b1c4947e4af5 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -65,6 +65,11 @@ public function testAutoRouteFindsDefaultControllerAndMethodGet() $this->assertSame('\\' . Index::class, $controller); $this->assertSame('getIndex', $method); $this->assertSame([], $params); + $this->assertSame([ + 'controller' => null, + 'method' => null, + 'params' => null, + ], $router->getPos()); } public function testAutoRouteFindsModuleDefaultControllerAndMethodGet() @@ -114,6 +119,11 @@ public function testAutoRouteFindsControllerWithFileAndMethod() $this->assertSame('\\' . Mycontroller::class, $controller); $this->assertSame('getSomemethod', $method); $this->assertSame([], $params); + $this->assertSame([ + 'controller' => 0, + 'method' => 1, + 'params' => null, + ], $router->getPos()); } public function testFindsControllerAndMethodAndParam() @@ -127,6 +137,11 @@ public function testFindsControllerAndMethodAndParam() $this->assertSame('\\' . Mycontroller::class, $controller); $this->assertSame('getSomemethod', $method); $this->assertSame(['a'], $params); + $this->assertSame([ + 'controller' => 0, + 'method' => 1, + 'params' => 2, + ], $router->getPos()); } public function testUriParamCountIsGreaterThanMethodParams() @@ -165,6 +180,11 @@ public function testAutoRouteFindsControllerWithSubfolder() $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Mycontroller::class, $controller); $this->assertSame('getSomemethod', $method); $this->assertSame([], $params); + $this->assertSame([ + 'controller' => 1, + 'method' => 2, + 'params' => null, + ], $router->getPos()); } public function testAutoRouteFindsControllerWithSubSubfolder() @@ -246,6 +266,11 @@ public function testAutoRouteFallbackToDefaultMethod() $this->assertSame('\\' . Index::class, $controller); $this->assertSame('getIndex', $method); $this->assertSame(['15'], $params); + $this->assertSame([ + 'controller' => 0, + 'method' => null, + 'params' => 1, + ], $router->getPos()); } public function testAutoRouteFallbackToDefaultControllerOneParam() @@ -259,6 +284,11 @@ public function testAutoRouteFallbackToDefaultControllerOneParam() $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); $this->assertSame('getIndex', $method); $this->assertSame(['15'], $params); + $this->assertSame([ + 'controller' => null, + 'method' => null, + 'params' => 1, + ], $router->getPos()); } public function testAutoRouteFallbackToDefaultControllerTwoParams() @@ -272,6 +302,11 @@ public function testAutoRouteFallbackToDefaultControllerTwoParams() $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); $this->assertSame('getIndex', $method); $this->assertSame(['15', '20'], $params); + $this->assertSame([ + 'controller' => null, + 'method' => null, + 'params' => 1, + ], $router->getPos()); } public function testAutoRouteFallbackToDefaultControllerNoParams() @@ -285,6 +320,11 @@ public function testAutoRouteFallbackToDefaultControllerNoParams() $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); $this->assertSame('getIndex', $method); $this->assertSame([], $params); + $this->assertSame([ + 'controller' => null, + 'method' => null, + 'params' => null, + ], $router->getPos()); } public function testAutoRouteRejectsSingleDot() From fb0d8d67db93a4539c7d928a259d5ecf0127f2e3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 2 Jun 2023 17:57:09 +0900 Subject: [PATCH 227/485] docs: add changelog and note --- user_guide_src/source/changelogs/v4.4.0.rst | 4 ++++ user_guide_src/source/incoming/routing.rst | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index dc1630e05a8f..85eb4517902e 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -151,6 +151,10 @@ Deprecations Bugs Fixed ********** +- **Auto Routing (Improved)**: In previous versions, when ``$translateURIDashes`` + is true, two URIs correspond to a single controller method, one URI for dashes + (e.g., **foo-bar**) and one URI for underscores (e.g., **foo_bar**). This bug + has been fixed. Now the URI for underscores (**foo_bar**) is not accessible. - **Output Buffering:** Bug fix with output buffering. See the repo's diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 516bb23fe133..00de9cf790fd 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -607,6 +607,12 @@ URI segments when used in Auto Routing, thus saving you additional route entries .. literalinclude:: routing/049.php +.. note:: When using Auto Routing (Improved), prior to v4.4.0, if + ``$translateURIDashes`` is true, two URIs correspond to a single controller + method, one URI for dashes (e.g., **foo-bar**) and one URI for underscores + (e.g., **foo_bar**). This was incorrect behavior. Since v4.4.0, the URI for + underscores (**foo_bar**) is not accessible. + .. _use-defined-routes-only: Use Defined Routes Only From 989ef707dd1972de23befcde674c70f9981b711d Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 2 Jun 2023 18:49:46 +0900 Subject: [PATCH 228/485] docs: add upgrade guide --- user_guide_src/source/installation/upgrade_440.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 5e7ab6856ce9..6a2c1960295d 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -60,6 +60,19 @@ by defining your own exception handler. See :ref:`custom-exception-handlers` for the detail. +Auto Routing (Improved) and translateURIDashes +============================================== + +When using Auto Routing (Improved) and ``$translateURIDashes`` is true +(``$routes->setTranslateURIDashes(true)``), in previous versions due to a bug +two URIs correspond to a single controller method, one URI for dashes +(e.g., **foo-bar**) and one URI for underscores (e.g., **foo_bar**). + +This bug was fixed and now URIs for underscores (**foo_bar**) is not accessible. + +If you have links to URIs for underscores (**foo_bar**), update them with URIs +for dashes (**foo-bar**). + Interface Changes ================= From 78b450cb9b5e2ef065080080130d62c199798127 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 14 Jun 2023 10:39:50 +0900 Subject: [PATCH 229/485] test: add new param for getRoute() --- tests/system/Router/AutoRouterImprovedTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index b1c4947e4af5..f18ab64b0ad4 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -402,7 +402,7 @@ public function testRejectsURIWithUnderscoreFolder() $router = $this->createNewAutoRouter(); - $router->getRoute('dash_folder'); + $router->getRoute('dash_folder', 'get'); } public function testRejectsURIWithUnderscoreController() @@ -414,7 +414,7 @@ public function testRejectsURIWithUnderscoreController() $router = $this->createNewAutoRouter(); - $router->getRoute('dash-folder/dash_controller/dash-method'); + $router->getRoute('dash-folder/dash_controller/dash-method', 'get'); } public function testRejectsURIWithUnderscoreMethod() @@ -426,7 +426,7 @@ public function testRejectsURIWithUnderscoreMethod() $router = $this->createNewAutoRouter(); - $router->getRoute('dash-folder/dash-controller/dash_method'); + $router->getRoute('dash-folder/dash-controller/dash_method', 'get'); } public function testPermitsURIWithUnderscoreParam() @@ -434,7 +434,7 @@ public function testPermitsURIWithUnderscoreParam() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('mycontroller/somemethod/a_b'); + = $router->getRoute('mycontroller/somemethod/a_b', 'get'); $this->assertNull($directory); $this->assertSame('\\' . Mycontroller::class, $controller); @@ -447,7 +447,7 @@ public function testDoesNotTranslateDashInParam() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('mycontroller/somemethod/a-b'); + = $router->getRoute('mycontroller/somemethod/a-b', 'get'); $this->assertNull($directory); $this->assertSame('\\' . Mycontroller::class, $controller); From c7b9da2a0f1ef71ef52a20044f5383f9d7f7fcf0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 14 Jun 2023 10:45:47 +0900 Subject: [PATCH 230/485] docs: remove @phpstan-ignore-line --- system/Router/AutoRouterImproved.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 6ba9f35b5576..fab6803cbcce 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -202,7 +202,7 @@ private function searchLastDefaultController(): bool $this->controller = $controller; $this->params = $params; - if ($params !== []) { // @phpstan-ignore-line + if ($params !== []) { $this->paramPos = $paramPos; } @@ -221,7 +221,7 @@ private function searchLastDefaultController(): bool $this->controller = $controller; $this->params = $params; - if ($params !== []) { // @phpstan-ignore-line + if ($params !== []) { $this->paramPos = 0; } From 9fa33dd4cdc0f8829812cf802e75beb9390d854e Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Wed, 14 Jun 2023 12:59:18 +0800 Subject: [PATCH 231/485] fix: CodeIgniter::$bufferLevel is not defined by default --- system/CodeIgniter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index b2b0a19bddf6..12bb8b60b612 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -331,7 +331,8 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon ); } - static::$cacheTTL = 0; + static::$cacheTTL = 0; + $this->bufferLevel = ob_get_level(); $this->startBenchmark(); From 49be3f1d23b470c40e0ea29867ea046bb01d8060 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 14 Feb 2023 19:40:01 +0900 Subject: [PATCH 232/485] fix: remove Session config items in Config\App --- app/Config/App.php | 107 ------------------ .../Generators/MigrationGenerator.php | 6 +- system/Config/Services.php | 4 +- system/Session/Handlers/BaseHandler.php | 13 +-- system/Session/Handlers/DatabaseHandler.php | 13 +-- system/Session/Handlers/MemcachedHandler.php | 8 +- system/Session/Handlers/RedisHandler.php | 19 +--- system/Session/Session.php | 25 ++-- 8 files changed, 24 insertions(+), 171 deletions(-) diff --git a/app/Config/App.php b/app/Config/App.php index 322526f06756..3eeb1573bae9 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -3,7 +3,6 @@ namespace Config; use CodeIgniter\Config\BaseConfig; -use CodeIgniter\Session\Handlers\FileHandler; class App extends BaseConfig { @@ -136,112 +135,6 @@ class App extends BaseConfig */ public bool $forceGlobalSecureRequests = false; - /** - * -------------------------------------------------------------------------- - * Session Driver - * -------------------------------------------------------------------------- - * - * The session storage driver to use: - * - `CodeIgniter\Session\Handlers\FileHandler` - * - `CodeIgniter\Session\Handlers\DatabaseHandler` - * - `CodeIgniter\Session\Handlers\MemcachedHandler` - * - `CodeIgniter\Session\Handlers\RedisHandler` - * - * @deprecated use Config\Session::$driver instead. - */ - public string $sessionDriver = FileHandler::class; - - /** - * -------------------------------------------------------------------------- - * Session Cookie Name - * -------------------------------------------------------------------------- - * - * The session cookie name, must contain only [0-9a-z_-] characters - * - * @deprecated use Config\Session::$cookieName instead. - */ - public string $sessionCookieName = 'ci_session'; - - /** - * -------------------------------------------------------------------------- - * Session Expiration - * -------------------------------------------------------------------------- - * - * The number of SECONDS you want the session to last. - * Setting to 0 (zero) means expire when the browser is closed. - * - * @deprecated use Config\Session::$expiration instead. - */ - public int $sessionExpiration = 7200; - - /** - * -------------------------------------------------------------------------- - * Session Save Path - * -------------------------------------------------------------------------- - * - * The location to save sessions to and is driver dependent. - * - * For the 'files' driver, it's a path to a writable directory. - * WARNING: Only absolute paths are supported! - * - * For the 'database' driver, it's a table name. - * Please read up the manual for the format with other session drivers. - * - * IMPORTANT: You are REQUIRED to set a valid save path! - * - * @deprecated use Config\Session::$savePath instead. - */ - public string $sessionSavePath = WRITEPATH . 'session'; - - /** - * -------------------------------------------------------------------------- - * Session Match IP - * -------------------------------------------------------------------------- - * - * Whether to match the user's IP address when reading the session data. - * - * WARNING: If you're using the database driver, don't forget to update - * your session table's PRIMARY KEY when changing this setting. - * - * @deprecated use Config\Session::$matchIP instead. - */ - public bool $sessionMatchIP = false; - - /** - * -------------------------------------------------------------------------- - * Session Time to Update - * -------------------------------------------------------------------------- - * - * How many seconds between CI regenerating the session ID. - * - * @deprecated use Config\Session::$timeToUpdate instead. - */ - public int $sessionTimeToUpdate = 300; - - /** - * -------------------------------------------------------------------------- - * Session Regenerate Destroy - * -------------------------------------------------------------------------- - * - * Whether to destroy session data associated with the old session ID - * when auto-regenerating the session ID. When set to FALSE, the data - * will be later deleted by the garbage collector. - * - * @deprecated use Config\Session::$regenerateDestroy instead. - */ - public bool $sessionRegenerateDestroy = false; - - /** - * -------------------------------------------------------------------------- - * Session Database Group - * -------------------------------------------------------------------------- - * - * DB Group for the database session. - * - * @deprecated use Config\Session::$DBGroup instead. - */ - public ?string $sessionDBGroup = null; - /** * -------------------------------------------------------------------------- * Reverse Proxy IPs diff --git a/system/Commands/Generators/MigrationGenerator.php b/system/Commands/Generators/MigrationGenerator.php index 817c16885205..5ebab33c3708 100644 --- a/system/Commands/Generators/MigrationGenerator.php +++ b/system/Commands/Generators/MigrationGenerator.php @@ -14,7 +14,6 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use CodeIgniter\CLI\GeneratorTrait; -use Config\App as AppConfig; use Config\Session as SessionConfig; /** @@ -109,13 +108,10 @@ protected function prepare(string $class): string $data['DBGroup'] = is_string($DBGroup) ? $DBGroup : 'default'; $data['DBDriver'] = config('Database')->{$data['DBGroup']}['DBDriver']; - /** @var AppConfig $config */ - $config = config('App'); /** @var SessionConfig|null $session */ $session = config('Session'); - $data['matchIP'] = ($session instanceof SessionConfig) - ? $session->matchIP : $config->sessionMatchIP; + $data['matchIP'] = $session->matchIP; } return $this->parseTemplate($class, [], [], $data); diff --git a/system/Config/Services.php b/system/Config/Services.php index 161a6b112757..1640c4f04bd8 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -653,10 +653,10 @@ public static function session(?App $config = null, bool $getShared = true) /** @var SessionConfig|null $sessionConfig */ $sessionConfig = config('Session'); - $driverName = $sessionConfig->driver ?? $config->sessionDriver; + $driverName = $sessionConfig->driver; if ($driverName === DatabaseHandler::class) { - $DBGroup = $sessionConfig->DBGroup ?? $config->sessionDBGroup ?? config(Database::class)->defaultGroup; + $DBGroup = $sessionConfig->DBGroup ?? config(Database::class)->defaultGroup; $db = Database::connect($DBGroup); $driver = $db->getPlatform(); diff --git a/system/Session/Handlers/BaseHandler.php b/system/Session/Handlers/BaseHandler.php index ab6d7e17bf73..00cf1413139c 100644 --- a/system/Session/Handlers/BaseHandler.php +++ b/system/Session/Handlers/BaseHandler.php @@ -111,16 +111,9 @@ public function __construct(AppConfig $config, string $ipAddress) $session = config('Session'); // Store Session configurations - if ($session instanceof SessionConfig) { - $this->cookieName = $session->cookieName; - $this->matchIP = $session->matchIP; - $this->savePath = $session->savePath; - } else { - // `Config/Session.php` is absence - $this->cookieName = $config->sessionCookieName; - $this->matchIP = $config->sessionMatchIP; - $this->savePath = $config->sessionSavePath; - } + $this->cookieName = $session->cookieName; + $this->matchIP = $session->matchIP; + $this->savePath = $session->savePath; /** @var CookieConfig $cookie */ $cookie = config('Cookie'); diff --git a/system/Session/Handlers/DatabaseHandler.php b/system/Session/Handlers/DatabaseHandler.php index b566a3b04d9e..2cc8917b5f27 100644 --- a/system/Session/Handlers/DatabaseHandler.php +++ b/system/Session/Handlers/DatabaseHandler.php @@ -77,16 +77,9 @@ public function __construct(AppConfig $config, string $ipAddress) $session = config('Session'); // Store Session configurations - if ($session instanceof SessionConfig) { - $this->DBGroup = $session->DBGroup ?? config(Database::class)->defaultGroup; - // Add sessionCookieName for multiple session cookies. - $this->idPrefix = $session->cookieName . ':'; - } else { - // `Config/Session.php` is absence - $this->DBGroup = $config->sessionDBGroup ?? config(Database::class)->defaultGroup; - // Add sessionCookieName for multiple session cookies. - $this->idPrefix = $config->sessionCookieName . ':'; - } + $this->DBGroup = $session->DBGroup ?? config(Database::class)->defaultGroup; + // Add sessionCookieName for multiple session cookies. + $this->idPrefix = $session->cookieName . ':'; $this->table = $this->savePath; if (empty($this->table)) { diff --git a/system/Session/Handlers/MemcachedHandler.php b/system/Session/Handlers/MemcachedHandler.php index e4c8c6e8c673..f7d6210a9293 100644 --- a/system/Session/Handlers/MemcachedHandler.php +++ b/system/Session/Handlers/MemcachedHandler.php @@ -58,19 +58,17 @@ public function __construct(AppConfig $config, string $ipAddress) { parent::__construct($config, $ipAddress); - /** @var SessionConfig|null $session */ + /** @var SessionConfig $session */ $session = config('Session'); - $this->sessionExpiration = ($session instanceof SessionConfig) - ? $session->expiration : $config->sessionExpiration; + $this->sessionExpiration = $session->expiration; if (empty($this->savePath)) { throw SessionException::forEmptySavepath(); } // Add sessionCookieName for multiple session cookies. - $this->keyPrefix .= ($session instanceof SessionConfig) - ? $session->cookieName : $config->sessionCookieName . ':'; + $this->keyPrefix .= $session->cookieName . ':'; if ($this->matchIP === true) { $this->keyPrefix .= $this->ipAddress . ':'; diff --git a/system/Session/Handlers/RedisHandler.php b/system/Session/Handlers/RedisHandler.php index 9910637663f5..d2cdb0337eae 100644 --- a/system/Session/Handlers/RedisHandler.php +++ b/system/Session/Handlers/RedisHandler.php @@ -75,20 +75,11 @@ public function __construct(AppConfig $config, string $ipAddress) $session = config('Session'); // Store Session configurations - if ($session instanceof SessionConfig) { - $this->sessionExpiration = empty($session->expiration) - ? (int) ini_get('session.gc_maxlifetime') - : (int) $session->expiration; - // Add sessionCookieName for multiple session cookies. - $this->keyPrefix .= $session->cookieName . ':'; - } else { - // `Config/Session.php` is absence - $this->sessionExpiration = empty($config->sessionExpiration) - ? (int) ini_get('session.gc_maxlifetime') - : (int) $config->sessionExpiration; - // Add sessionCookieName for multiple session cookies. - $this->keyPrefix .= $config->sessionCookieName . ':'; - } + $this->sessionExpiration = empty($session->expiration) + ? (int) ini_get('session.gc_maxlifetime') + : (int) $session->expiration; + // Add sessionCookieName for multiple session cookies. + $this->keyPrefix .= $session->cookieName . ':'; $this->setSavePath(); diff --git a/system/Session/Session.php b/system/Session/Session.php index 75fba76e478f..e4b4a5fb2519 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -169,24 +169,13 @@ public function __construct(SessionHandlerInterface $driver, App $config) $session = config('Session'); // Store Session configurations - if ($session instanceof SessionConfig) { - $this->sessionDriverName = $session->driver; - $this->sessionCookieName = $session->cookieName ?? $this->sessionCookieName; - $this->sessionExpiration = $session->expiration ?? $this->sessionExpiration; - $this->sessionSavePath = $session->savePath; - $this->sessionMatchIP = $session->matchIP ?? $this->sessionMatchIP; - $this->sessionTimeToUpdate = $session->timeToUpdate ?? $this->sessionTimeToUpdate; - $this->sessionRegenerateDestroy = $session->regenerateDestroy ?? $this->sessionRegenerateDestroy; - } else { - // `Config/Session.php` is absence - $this->sessionDriverName = $config->sessionDriver; - $this->sessionCookieName = $config->sessionCookieName ?? $this->sessionCookieName; - $this->sessionExpiration = $config->sessionExpiration ?? $this->sessionExpiration; - $this->sessionSavePath = $config->sessionSavePath; - $this->sessionMatchIP = $config->sessionMatchIP ?? $this->sessionMatchIP; - $this->sessionTimeToUpdate = $config->sessionTimeToUpdate ?? $this->sessionTimeToUpdate; - $this->sessionRegenerateDestroy = $config->sessionRegenerateDestroy ?? $this->sessionRegenerateDestroy; - } + $this->sessionDriverName = $session->driver; + $this->sessionCookieName = $session->cookieName ?? $this->sessionCookieName; + $this->sessionExpiration = $session->expiration ?? $this->sessionExpiration; + $this->sessionSavePath = $session->savePath; + $this->sessionMatchIP = $session->matchIP ?? $this->sessionMatchIP; + $this->sessionTimeToUpdate = $session->timeToUpdate ?? $this->sessionTimeToUpdate; + $this->sessionRegenerateDestroy = $session->regenerateDestroy ?? $this->sessionRegenerateDestroy; /** @var CookieConfig $cookie */ $cookie = config('Cookie'); From 75fbf5650f0737e4c1a01fab9cf7e4a0f48e66af Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 14 Feb 2023 20:04:09 +0900 Subject: [PATCH 233/485] fix: remove Config\App from constructors in Session and Session Handlers --- system/Config/Services.php | 14 ++++++------ system/Session/Handlers/BaseHandler.php | 12 ++++------ system/Session/Handlers/DatabaseHandler.php | 10 +++------ system/Session/Handlers/FileHandler.php | 4 ++-- system/Session/Handlers/MemcachedHandler.php | 10 +++------ system/Session/Handlers/RedisHandler.php | 12 ++++------ system/Session/Session.php | 7 ++---- system/Test/CIUnitTestCase.php | 2 +- tests/system/CommonFunctionsTest.php | 21 +++++++++--------- .../SecurityCSRFSessionRandomizeTokenTest.php | 22 +++++++++---------- .../Security/SecurityCSRFSessionTest.php | 22 +++++++++---------- tests/system/Session/SessionTest.php | 2 +- 12 files changed, 60 insertions(+), 78 deletions(-) diff --git a/system/Config/Services.php b/system/Config/Services.php index 1640c4f04bd8..fafeb3ade954 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -638,6 +638,8 @@ public static function security(?App $config = null, bool $getShared = true) * Return the session manager. * * @return Session + * + * @TODO replace the first parameter type `?App` with `?SessionConfig` */ public static function session(?App $config = null, bool $getShared = true) { @@ -645,18 +647,16 @@ public static function session(?App $config = null, bool $getShared = true) return static::getSharedInstance('session', $config); } - $config ??= config('App'); - assert($config instanceof App); - $logger = AppServices::logger(); - /** @var SessionConfig|null $sessionConfig */ - $sessionConfig = config('Session'); + /** @var SessionConfig $config */ + $config = config('Session'); + assert($config instanceof SessionConfig, 'Missing "Config/Session.php".'); - $driverName = $sessionConfig->driver; + $driverName = $config->driver; if ($driverName === DatabaseHandler::class) { - $DBGroup = $sessionConfig->DBGroup ?? config(Database::class)->defaultGroup; + $DBGroup = $config->DBGroup ?? config(Database::class)->defaultGroup; $db = Database::connect($DBGroup); $driver = $db->getPlatform(); diff --git a/system/Session/Handlers/BaseHandler.php b/system/Session/Handlers/BaseHandler.php index 00cf1413139c..df62f72303ba 100644 --- a/system/Session/Handlers/BaseHandler.php +++ b/system/Session/Handlers/BaseHandler.php @@ -11,7 +11,6 @@ namespace CodeIgniter\Session\Handlers; -use Config\App as AppConfig; use Config\Cookie as CookieConfig; use Config\Session as SessionConfig; use Psr\Log\LoggerAwareTrait; @@ -105,15 +104,12 @@ abstract class BaseHandler implements SessionHandlerInterface */ protected $ipAddress; - public function __construct(AppConfig $config, string $ipAddress) + public function __construct(SessionConfig $config, string $ipAddress) { - /** @var SessionConfig|null $session */ - $session = config('Session'); - // Store Session configurations - $this->cookieName = $session->cookieName; - $this->matchIP = $session->matchIP; - $this->savePath = $session->savePath; + $this->cookieName = $config->cookieName; + $this->matchIP = $config->matchIP; + $this->savePath = $config->savePath; /** @var CookieConfig $cookie */ $cookie = config('Cookie'); diff --git a/system/Session/Handlers/DatabaseHandler.php b/system/Session/Handlers/DatabaseHandler.php index 2cc8917b5f27..edde55ca34c6 100644 --- a/system/Session/Handlers/DatabaseHandler.php +++ b/system/Session/Handlers/DatabaseHandler.php @@ -14,7 +14,6 @@ use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Session\Exceptions\SessionException; -use Config\App as AppConfig; use Config\Database; use Config\Session as SessionConfig; use ReturnTypeWillChange; @@ -69,17 +68,14 @@ class DatabaseHandler extends BaseHandler /** * @throws SessionException */ - public function __construct(AppConfig $config, string $ipAddress) + public function __construct(SessionConfig $config, string $ipAddress) { parent::__construct($config, $ipAddress); - /** @var SessionConfig|null $session */ - $session = config('Session'); - // Store Session configurations - $this->DBGroup = $session->DBGroup ?? config(Database::class)->defaultGroup; + $this->DBGroup = $config->DBGroup ?? config(Database::class)->defaultGroup; // Add sessionCookieName for multiple session cookies. - $this->idPrefix = $session->cookieName . ':'; + $this->idPrefix = $config->cookieName . ':'; $this->table = $this->savePath; if (empty($this->table)) { diff --git a/system/Session/Handlers/FileHandler.php b/system/Session/Handlers/FileHandler.php index cc8a6694f692..9eb642408381 100644 --- a/system/Session/Handlers/FileHandler.php +++ b/system/Session/Handlers/FileHandler.php @@ -13,7 +13,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Session\Exceptions\SessionException; -use Config\App as AppConfig; +use Config\Session as SessionConfig; use ReturnTypeWillChange; /** @@ -63,7 +63,7 @@ class FileHandler extends BaseHandler */ protected $sessionIDRegex = ''; - public function __construct(AppConfig $config, string $ipAddress) + public function __construct(SessionConfig $config, string $ipAddress) { parent::__construct($config, $ipAddress); diff --git a/system/Session/Handlers/MemcachedHandler.php b/system/Session/Handlers/MemcachedHandler.php index f7d6210a9293..abccfafb5ba4 100644 --- a/system/Session/Handlers/MemcachedHandler.php +++ b/system/Session/Handlers/MemcachedHandler.php @@ -13,7 +13,6 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Session\Exceptions\SessionException; -use Config\App as AppConfig; use Config\Session as SessionConfig; use Memcached; use ReturnTypeWillChange; @@ -54,21 +53,18 @@ class MemcachedHandler extends BaseHandler /** * @throws SessionException */ - public function __construct(AppConfig $config, string $ipAddress) + public function __construct(SessionConfig $config, string $ipAddress) { parent::__construct($config, $ipAddress); - /** @var SessionConfig $session */ - $session = config('Session'); - - $this->sessionExpiration = $session->expiration; + $this->sessionExpiration = $config->expiration; if (empty($this->savePath)) { throw SessionException::forEmptySavepath(); } // Add sessionCookieName for multiple session cookies. - $this->keyPrefix .= $session->cookieName . ':'; + $this->keyPrefix .= $config->cookieName . ':'; if ($this->matchIP === true) { $this->keyPrefix .= $this->ipAddress . ':'; diff --git a/system/Session/Handlers/RedisHandler.php b/system/Session/Handlers/RedisHandler.php index d2cdb0337eae..f5360e96c48a 100644 --- a/system/Session/Handlers/RedisHandler.php +++ b/system/Session/Handlers/RedisHandler.php @@ -13,7 +13,6 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Session\Exceptions\SessionException; -use Config\App as AppConfig; use Config\Session as SessionConfig; use Redis; use RedisException; @@ -67,19 +66,16 @@ class RedisHandler extends BaseHandler * * @throws SessionException */ - public function __construct(AppConfig $config, string $ipAddress) + public function __construct(SessionConfig $config, string $ipAddress) { parent::__construct($config, $ipAddress); - /** @var SessionConfig|null $session */ - $session = config('Session'); - // Store Session configurations - $this->sessionExpiration = empty($session->expiration) + $this->sessionExpiration = empty($config->expiration) ? (int) ini_get('session.gc_maxlifetime') - : (int) $session->expiration; + : (int) $config->expiration; // Add sessionCookieName for multiple session cookies. - $this->keyPrefix .= $session->cookieName . ':'; + $this->keyPrefix .= $config->cookieName . ':'; $this->setSavePath(); diff --git a/system/Session/Session.php b/system/Session/Session.php index e4b4a5fb2519..6d77ac3e82c7 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -62,7 +62,7 @@ class Session implements SessionInterface protected $sessionExpiration = 7200; /** - * The location to save sessions to, driver dependent.. + * The location to save sessions to, driver dependent. * * For the 'files' driver, it's a path to a writable directory. * WARNING: Only absolute paths are supported! @@ -161,13 +161,10 @@ class Session implements SessionInterface * * Extract configuration settings and save them here. */ - public function __construct(SessionHandlerInterface $driver, App $config) + public function __construct(SessionHandlerInterface $driver, SessionConfig $session) { $this->driver = $driver; - /** @var SessionConfig|null $session */ - $session = config('Session'); - // Store Session configurations $this->sessionDriverName = $session->driver; $this->sessionCookieName = $session->cookieName ?? $this->sessionCookieName; diff --git a/system/Test/CIUnitTestCase.php b/system/Test/CIUnitTestCase.php index bfde56faf6ff..32d782f69ec2 100644 --- a/system/Test/CIUnitTestCase.php +++ b/system/Test/CIUnitTestCase.php @@ -333,7 +333,7 @@ protected function mockSession() { $_SESSION = []; - $config = config('App'); + $config = config('Session'); $session = new MockSession(new ArrayHandler($config, '0.0.0.0'), $config); Services::injectMock('session', $session); diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index a40e4cd8d438..4c174ecd12f4 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -34,6 +34,7 @@ use Config\Modules; use Config\Routing; use Config\Services; +use Config\Session as SessionConfig; use Kint; use RuntimeException; use stdClass; @@ -517,20 +518,20 @@ public function testSlashItemThrowsErrorOnNonStringableItem() protected function injectSessionMock() { - $appConfig = new App(); + $sessionConfig = new SessionConfig(); $defaults = [ - 'sessionDriver' => FileHandler::class, - 'sessionCookieName' => 'ci_session', - 'sessionExpiration' => 7200, - 'sessionSavePath' => '', - 'sessionMatchIP' => false, - 'sessionTimeToUpdate' => 300, - 'sessionRegenerateDestroy' => false, + 'driver' => FileHandler::class, + 'cookieName' => 'ci_session', + 'expiration' => 7200, + 'savePath' => '', + 'matchIP' => false, + 'timeToUpdate' => 300, + 'regenerateDestroy' => false, ]; foreach ($defaults as $key => $config) { - $appConfig->{$key} = $config; + $sessionConfig->{$key} = $config; } $cookie = new Cookie(); @@ -546,7 +547,7 @@ protected function injectSessionMock() } Factories::injectMock('config', 'Cookie', $cookie); - $session = new MockSession(new FileHandler($appConfig, '127.0.0.1'), $appConfig); + $session = new MockSession(new FileHandler($sessionConfig, '127.0.0.1'), $sessionConfig); $session->setLogger(new TestLogger(new Logger())); BaseService::injectMock('session', $session); } diff --git a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php index f8f3fc01fbaa..36bd5ea97f1c 100644 --- a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php +++ b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php @@ -25,10 +25,10 @@ use CodeIgniter\Test\Mock\MockSecurity; use CodeIgniter\Test\Mock\MockSession; use CodeIgniter\Test\TestLogger; -use Config\App as AppConfig; use Config\Cookie; use Config\Logger as LoggerConfig; use Config\Security as SecurityConfig; +use Config\Session as SessionConfig; /** * @runTestsInSeparateProcesses @@ -69,20 +69,20 @@ protected function setUp(): void private function createSession($options = []): Session { $defaults = [ - 'sessionDriver' => FileHandler::class, - 'sessionCookieName' => 'ci_session', - 'sessionExpiration' => 7200, - 'sessionSavePath' => '', - 'sessionMatchIP' => false, - 'sessionTimeToUpdate' => 300, - 'sessionRegenerateDestroy' => false, + 'driver' => FileHandler::class, + 'cookieName' => 'ci_session', + 'expiration' => 7200, + 'savePath' => '', + 'matchIP' => false, + 'timeToUpdate' => 300, + 'regenerateDestroy' => false, ]; $config = array_merge($defaults, $options); - $appConfig = new AppConfig(); + $sessionConfig = new SessionConfig(); foreach ($config as $key => $c) { - $appConfig->{$key} = $c; + $sessionConfig->{$key} = $c; } $cookie = new Cookie(); @@ -98,7 +98,7 @@ private function createSession($options = []): Session } Factories::injectMock('config', 'Cookie', $cookie); - $session = new MockSession(new ArrayHandler($appConfig, '127.0.0.1'), $appConfig); + $session = new MockSession(new ArrayHandler($sessionConfig, '127.0.0.1'), $sessionConfig); $session->setLogger(new TestLogger(new LoggerConfig())); return $session; diff --git a/tests/system/Security/SecurityCSRFSessionTest.php b/tests/system/Security/SecurityCSRFSessionTest.php index 495f9e58b06d..d6f2216e29f6 100644 --- a/tests/system/Security/SecurityCSRFSessionTest.php +++ b/tests/system/Security/SecurityCSRFSessionTest.php @@ -24,10 +24,10 @@ use CodeIgniter\Test\Mock\MockAppConfig; use CodeIgniter\Test\Mock\MockSession; use CodeIgniter\Test\TestLogger; -use Config\App as AppConfig; use Config\Cookie; use Config\Logger as LoggerConfig; use Config\Security as SecurityConfig; +use Config\Session as SessionConfig; /** * @runTestsInSeparateProcesses @@ -62,20 +62,20 @@ protected function setUp(): void private function createSession($options = []): Session { $defaults = [ - 'sessionDriver' => FileHandler::class, - 'sessionCookieName' => 'ci_session', - 'sessionExpiration' => 7200, - 'sessionSavePath' => '', - 'sessionMatchIP' => false, - 'sessionTimeToUpdate' => 300, - 'sessionRegenerateDestroy' => false, + 'driver' => FileHandler::class, + 'cookieName' => 'ci_session', + 'expiration' => 7200, + 'savePath' => '', + 'matchIP' => false, + 'timeToUpdate' => 300, + 'regenerateDestroy' => false, ]; $config = array_merge($defaults, $options); - $appConfig = new AppConfig(); + $sessionConfig = new SessionConfig(); foreach ($config as $key => $c) { - $appConfig->{$key} = $c; + $sessionConfig->{$key} = $c; } $cookie = new Cookie(); @@ -91,7 +91,7 @@ private function createSession($options = []): Session } Factories::injectMock('config', 'Cookie', $cookie); - $session = new MockSession(new ArrayHandler($appConfig, '127.0.0.1'), $appConfig); + $session = new MockSession(new ArrayHandler($sessionConfig, '127.0.0.1'), $sessionConfig); $session->setLogger(new TestLogger(new LoggerConfig())); return $session; diff --git a/tests/system/Session/SessionTest.php b/tests/system/Session/SessionTest.php index 8e0170d0fee1..de093e18a8e1 100644 --- a/tests/system/Session/SessionTest.php +++ b/tests/system/Session/SessionTest.php @@ -62,7 +62,7 @@ protected function getInstance($options = []) } Factories::injectMock('config', 'Session', $sessionConfig); - $session = new MockSession(new FileHandler($appConfig, '127.0.0.1'), $appConfig); + $session = new MockSession(new FileHandler($sessionConfig, '127.0.0.1'), $sessionConfig); $session->setLogger(new TestLogger(new LoggerConfig())); return $session; From 01e1e51c28cf11b9424b7ebdf2b7359db1ab1328 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 10:34:18 +0900 Subject: [PATCH 234/485] refactor: add property for SessionConfig and use it --- system/Session/Session.php | 70 +++++++++++++++++--------------- system/Test/Mock/MockSession.php | 2 +- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/system/Session/Session.php b/system/Session/Session.php index 6d77ac3e82c7..8c5a9d96ecbc 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -43,6 +43,8 @@ class Session implements SessionInterface * The storage driver to use: files, database, redis, memcached * * @var string + * + * @deprecated Use $this->config->driver. */ protected $sessionDriverName; @@ -50,6 +52,8 @@ class Session implements SessionInterface * The session cookie name, must contain only [0-9a-z_-] characters. * * @var string + * + * @deprecated Use $this->config->cookieName. */ protected $sessionCookieName = 'ci_session'; @@ -58,6 +62,8 @@ class Session implements SessionInterface * Setting it to 0 (zero) means expire when the browser is closed. * * @var int + * + * @deprecated Use $this->config->expiration. */ protected $sessionExpiration = 7200; @@ -74,6 +80,8 @@ class Session implements SessionInterface * IMPORTANT: You are REQUIRED to set a valid save path! * * @var string + * + * @deprecated Use $this->config->savePath. */ protected $sessionSavePath; @@ -84,6 +92,8 @@ class Session implements SessionInterface * your session table's PRIMARY KEY when changing this setting. * * @var bool + * + * @deprecated Use $this->config->matchIP. */ protected $sessionMatchIP = false; @@ -91,6 +101,8 @@ class Session implements SessionInterface * How many seconds between CI regenerating the session ID. * * @var int + * + * @deprecated Use $this->config->timeToUpdate. */ protected $sessionTimeToUpdate = 300; @@ -100,6 +112,8 @@ class Session implements SessionInterface * will be later deleted by the garbage collector. * * @var bool + * + * @deprecated Use $this->config->regenerateDestroy. */ protected $sessionRegenerateDestroy = false; @@ -156,6 +170,11 @@ class Session implements SessionInterface */ protected $sidRegexp; + /** + * Session Config + */ + protected SessionConfig $config; + /** * Constructor. * @@ -165,20 +184,13 @@ public function __construct(SessionHandlerInterface $driver, SessionConfig $sess { $this->driver = $driver; - // Store Session configurations - $this->sessionDriverName = $session->driver; - $this->sessionCookieName = $session->cookieName ?? $this->sessionCookieName; - $this->sessionExpiration = $session->expiration ?? $this->sessionExpiration; - $this->sessionSavePath = $session->savePath; - $this->sessionMatchIP = $session->matchIP ?? $this->sessionMatchIP; - $this->sessionTimeToUpdate = $session->timeToUpdate ?? $this->sessionTimeToUpdate; - $this->sessionRegenerateDestroy = $session->regenerateDestroy ?? $this->sessionRegenerateDestroy; + $this->config = $session; /** @var CookieConfig $cookie */ $cookie = config('Cookie'); - $this->cookie = (new Cookie($this->sessionCookieName, '', [ - 'expires' => $this->sessionExpiration === 0 ? 0 : Time::now()->getTimestamp() + $this->sessionExpiration, + $this->cookie = (new Cookie($this->config->cookieName, '', [ + 'expires' => $this->config->expiration === 0 ? 0 : Time::now()->getTimestamp() + $this->config->expiration, 'path' => $cookie->path, 'domain' => $cookie->domain, 'secure' => $cookie->secure, @@ -221,32 +233,32 @@ public function start() $this->setSaveHandler(); // Sanitize the cookie, because apparently PHP doesn't do that for userspace handlers - if (isset($_COOKIE[$this->sessionCookieName]) - && (! is_string($_COOKIE[$this->sessionCookieName]) || ! preg_match('#\A' . $this->sidRegexp . '\z#', $_COOKIE[$this->sessionCookieName])) + if (isset($_COOKIE[$this->config->cookieName]) + && (! is_string($_COOKIE[$this->config->cookieName]) || ! preg_match('#\A' . $this->sidRegexp . '\z#', $_COOKIE[$this->config->cookieName])) ) { - unset($_COOKIE[$this->sessionCookieName]); + unset($_COOKIE[$this->config->cookieName]); } $this->startSession(); // Is session ID auto-regeneration configured? (ignoring ajax requests) if ((empty($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest') - && ($regenerateTime = $this->sessionTimeToUpdate) > 0 + && ($regenerateTime = $this->config->timeToUpdate) > 0 ) { if (! isset($_SESSION['__ci_last_regenerate'])) { $_SESSION['__ci_last_regenerate'] = Time::now()->getTimestamp(); } elseif ($_SESSION['__ci_last_regenerate'] < (Time::now()->getTimestamp() - $regenerateTime)) { - $this->regenerate((bool) $this->sessionRegenerateDestroy); + $this->regenerate((bool) $this->config->regenerateDestroy); } } // Another work-around ... PHP doesn't seem to send the session cookie // unless it is being currently created or regenerated - elseif (isset($_COOKIE[$this->sessionCookieName]) && $_COOKIE[$this->sessionCookieName] === session_id()) { + elseif (isset($_COOKIE[$this->config->cookieName]) && $_COOKIE[$this->config->cookieName] === session_id()) { $this->setCookie(); } $this->initVars(); - $this->logger->info("Session: Class initialized using '" . $this->sessionDriverName . "' driver."); + $this->logger->info("Session: Class initialized using '" . $this->config->driver . "' driver."); return $this; } @@ -268,16 +280,12 @@ public function stop() */ protected function configure() { - if (empty($this->sessionCookieName)) { - $this->sessionCookieName = ini_get('session.name'); - } else { - ini_set('session.name', $this->sessionCookieName); - } + ini_set('session.name', $this->config->cookieName); $sameSite = $this->cookie->getSameSite() ?: ucfirst(Cookie::SAMESITE_LAX); $params = [ - 'lifetime' => $this->sessionExpiration, + 'lifetime' => $this->config->expiration, 'path' => $this->cookie->getPath(), 'domain' => $this->cookie->getDomain(), 'secure' => $this->cookie->isSecure(), @@ -288,14 +296,12 @@ protected function configure() ini_set('session.cookie_samesite', $sameSite); session_set_cookie_params($params); - if (! isset($this->sessionExpiration)) { - $this->sessionExpiration = (int) ini_get('session.gc_maxlifetime'); - } elseif ($this->sessionExpiration > 0) { - ini_set('session.gc_maxlifetime', (string) $this->sessionExpiration); + if ($this->config->expiration > 0) { + ini_set('session.gc_maxlifetime', (string) $this->config->expiration); } - if (! empty($this->sessionSavePath)) { - ini_set('session.save_path', $this->sessionSavePath); + if (! empty($this->config->savePath)) { + ini_set('session.save_path', $this->config->savePath); } // Security is king @@ -402,12 +408,12 @@ private function removeOldSessionCookie(): void $response = Services::response(); $cookieStoreInResponse = $response->getCookieStore(); - if (! $cookieStoreInResponse->has($this->sessionCookieName)) { + if (! $cookieStoreInResponse->has($this->config->cookieName)) { return; } // CookieStore is immutable. - $newCookieStore = $cookieStoreInResponse->remove($this->sessionCookieName); + $newCookieStore = $cookieStoreInResponse->remove($this->config->cookieName); // But clear() method clears cookies in the object (not immutable). $cookieStoreInResponse->clear(); @@ -921,7 +927,7 @@ protected function startSession() */ protected function setCookie() { - $expiration = $this->sessionExpiration === 0 ? 0 : Time::now()->getTimestamp() + $this->sessionExpiration; + $expiration = $this->config->expiration === 0 ? 0 : Time::now()->getTimestamp() + $this->config->expiration; $this->cookie = $this->cookie->withValue(session_id())->withExpires($expiration); $response = Services::response(); diff --git a/system/Test/Mock/MockSession.php b/system/Test/Mock/MockSession.php index f5290f26525d..9f558e1034ad 100644 --- a/system/Test/Mock/MockSession.php +++ b/system/Test/Mock/MockSession.php @@ -57,7 +57,7 @@ protected function startSession() */ protected function setCookie() { - $expiration = $this->sessionExpiration === 0 ? 0 : Time::now()->getTimestamp() + $this->sessionExpiration; + $expiration = $this->config->expiration === 0 ? 0 : Time::now()->getTimestamp() + $this->config->expiration; $this->cookie = $this->cookie->withValue(session_id())->withExpires($expiration); $this->cookies[] = $this->cookie; From 6a80edddeca1e01ca384c79afee952888443ee0c Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 10:43:27 +0900 Subject: [PATCH 235/485] test: remove unneeded variable --- tests/system/Session/SessionTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/system/Session/SessionTest.php b/tests/system/Session/SessionTest.php index de093e18a8e1..921f9b80568d 100644 --- a/tests/system/Session/SessionTest.php +++ b/tests/system/Session/SessionTest.php @@ -17,7 +17,6 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockSession; use CodeIgniter\Test\TestLogger; -use Config\App as AppConfig; use Config\Cookie as CookieConfig; use Config\Logger as LoggerConfig; use Config\Session as SessionConfig; @@ -43,8 +42,6 @@ protected function setUp(): void protected function getInstance($options = []) { - $appConfig = new AppConfig(); - $defaults = [ 'driver' => FileHandler::class, 'cookieName' => 'ci_session', From 9db8130f94caa5a93c027e80514acf443367462a Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 10:59:21 +0900 Subject: [PATCH 236/485] docs: add changelog and upgrading guide --- user_guide_src/source/changelogs/v4.4.0.rst | 10 ++++++++++ user_guide_src/source/installation/upgrade_440.rst | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index dc1630e05a8f..a90187940cf1 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -47,6 +47,11 @@ Method Signature Changes ``RouteCollection::__construct()``. - **Validation:** The method signature of ``Validation::check()`` has been changed. The ``string`` typehint on the ``$rule`` parameter was removed. +- **Session:** The second parameter of ``Session::__construct()`` has been + changed from ``Config\App`` to ``Config\Session``. +- **Session:** The first parameter of ``__construct()`` in ``BaseHandler``, + ``DatabaseHandler``, ``FileHandler``, ``MemcachedHandler``, and ``RedisHandler`` + has been changed from ``Config\App`` to ``Config\Session``. Enhancements ************ @@ -127,6 +132,7 @@ Changes - **Images:** The default quality for WebP in ``GDHandler`` has been changed from 80 to 90. - **Config:** The deprecated Cookie items in **app/Config/App.php** has been removed. +- **Config:** The deprecated Session items in **app/Config/App.php** has been removed. - **Config:** Routing settings have been moved to **app/Config/Routing.php** config file. See :ref:`Upgrading Guide `. - **DownloadResponse:** When generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. @@ -147,6 +153,10 @@ Deprecations - **Autoloader:** ``Autoloader::sanitizeFilename()`` is deprecated. - **CodeIgniter:** ``CodeIgniter::$returnResponse`` property is deprecated. No longer used. - **RedirectException:** ``\CodeIgniter\Router\Exceptions\RedirectException`` is deprecated. Use \CodeIgniter\HTTP\Exceptions\RedirectException instead. +- **Session:** The property ``$sessionDriverName``, ``$sessionCookieName``, + ``$sessionExpiration``, ``$sessionSavePath``, ``$sessionMatchIP``, + ``$sessionTimeToUpdate``, and ``$sessionRegenerateDestroy`` in ``Session`` are + deprecated, and no longer used. Use ``$config`` instead. Bugs Fixed ********** diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 5e7ab6856ce9..9c765f24434b 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -100,6 +100,16 @@ The Cookie config items in **app/Config/App.php** are no longer used. 2. Remove the properties (from ``$cookiePrefix`` to ``$cookieSameSite``) in **app/Config/App.php**. +app/Config/Session.php +---------------------- + +The Session config items in **app/Config/App.php** are no longer used. + +1. Copy **app/Config/Session.php** from the new framework to your **app/Config** + directory, and configure it. +2. Remove the properties (from ``$sessionDriver`` to ``$sessionDBGroup``) in + **app/Config/App.php**. + Breaking Enhancements ********************* From 3a84ef4f36e01a217f5d669dc9d9737532f1883b Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 11:02:59 +0900 Subject: [PATCH 237/485] test: update parameter --- tests/system/Session/Handlers/Database/MySQLiHandlerTest.php | 5 +---- .../system/Session/Handlers/Database/PostgreHandlerTest.php | 5 +---- tests/system/Session/Handlers/Database/RedisHandlerTest.php | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/system/Session/Handlers/Database/MySQLiHandlerTest.php b/tests/system/Session/Handlers/Database/MySQLiHandlerTest.php index b18f1b047eef..19266f4462b5 100644 --- a/tests/system/Session/Handlers/Database/MySQLiHandlerTest.php +++ b/tests/system/Session/Handlers/Database/MySQLiHandlerTest.php @@ -11,8 +11,6 @@ namespace CodeIgniter\Session\Handlers\Database; -use CodeIgniter\Config\Factories; -use Config\App as AppConfig; use Config\Database as DatabaseConfig; use Config\Session as SessionConfig; @@ -49,8 +47,7 @@ protected function getInstance($options = []) foreach ($config as $key => $value) { $sessionConfig->{$key} = $value; } - Factories::injectMock('config', 'Session', $sessionConfig); - return new MySQLiHandler(new AppConfig(), $this->userIpAddress); + return new MySQLiHandler($sessionConfig, $this->userIpAddress); } } diff --git a/tests/system/Session/Handlers/Database/PostgreHandlerTest.php b/tests/system/Session/Handlers/Database/PostgreHandlerTest.php index f9db847faab0..3e5fe5a3b10b 100644 --- a/tests/system/Session/Handlers/Database/PostgreHandlerTest.php +++ b/tests/system/Session/Handlers/Database/PostgreHandlerTest.php @@ -11,8 +11,6 @@ namespace CodeIgniter\Session\Handlers\Database; -use CodeIgniter\Config\Factories; -use Config\App as AppConfig; use Config\Database as DatabaseConfig; use Config\Session as SessionConfig; @@ -49,8 +47,7 @@ protected function getInstance($options = []) foreach ($config as $key => $value) { $sessionConfig->{$key} = $value; } - Factories::injectMock('config', 'Session', $sessionConfig); - return new PostgreHandler(new AppConfig(), $this->userIpAddress); + return new PostgreHandler($sessionConfig, $this->userIpAddress); } } diff --git a/tests/system/Session/Handlers/Database/RedisHandlerTest.php b/tests/system/Session/Handlers/Database/RedisHandlerTest.php index b5d67a768c47..893f249ee22c 100644 --- a/tests/system/Session/Handlers/Database/RedisHandlerTest.php +++ b/tests/system/Session/Handlers/Database/RedisHandlerTest.php @@ -11,10 +11,8 @@ namespace CodeIgniter\Session\Handlers\Database; -use CodeIgniter\Config\Factories; use CodeIgniter\Session\Handlers\RedisHandler; use CodeIgniter\Test\CIUnitTestCase; -use Config\App as AppConfig; use Config\Session as SessionConfig; use Redis; @@ -49,9 +47,8 @@ protected function getInstance($options = []) foreach ($config as $key => $value) { $sessionConfig->{$key} = $value; } - Factories::injectMock('config', 'Session', $sessionConfig); - return new RedisHandler(new AppConfig(), $this->userIpAddress); + return new RedisHandler($sessionConfig, $this->userIpAddress); } public function testSavePathWithoutProtocol() From 33829746efd96b2b51dc9307ff643569b3f14836 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 11:07:32 +0900 Subject: [PATCH 238/485] refactor: revert parameter variable name --- system/Session/Session.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Session/Session.php b/system/Session/Session.php index 8c5a9d96ecbc..4d139c18aa02 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -180,11 +180,11 @@ class Session implements SessionInterface * * Extract configuration settings and save them here. */ - public function __construct(SessionHandlerInterface $driver, SessionConfig $session) + public function __construct(SessionHandlerInterface $driver, SessionConfig $config) { $this->driver = $driver; - $this->config = $session; + $this->config = $config; /** @var CookieConfig $cookie */ $cookie = config('Cookie'); From 24fc2553d582efb5ac40e7c0124c09240b23855e Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 11:47:14 +0900 Subject: [PATCH 239/485] test: update out of dated test --- tests/system/CommonFunctionsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 4c174ecd12f4..884b2819f912 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -496,7 +496,7 @@ public function testReallyWritable() public function testSlashItem() { $this->assertSame('en/', slash_item('defaultLocale')); // en - $this->assertSame('7200/', slash_item('sessionExpiration')); // int 7200 + $this->assertSame('7200/', slash_item('CSRFExpire')); // int 7200 $this->assertSame('', slash_item('negotiateLocale')); // false } From 7dda3b12e0ce69256c8d4342a6e1025e58c4f636 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 11:29:05 +0900 Subject: [PATCH 240/485] chore: remove PHPStan ignored error pattern --- phpstan-baseline.neon.dist | 5 ----- 1 file changed, 5 deletions(-) diff --git a/phpstan-baseline.neon.dist b/phpstan-baseline.neon.dist index a32d4fc11a79..2d60b2b599d2 100644 --- a/phpstan-baseline.neon.dist +++ b/phpstan-baseline.neon.dist @@ -230,11 +230,6 @@ parameters: count: 1 path: system/Router/Router.php - - - message: "#^Property CodeIgniter\\\\Session\\\\Session\\:\\:\\$sessionExpiration \\(int\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: system/Session/Session.php - - message: "#^Access to an undefined property object\\:\\:\\$createdField\\.$#" count: 1 From cd4fbe1fbaab690953d9dbb5357da0832239cee4 Mon Sep 17 00:00:00 2001 From: BennyBPB Date: Sun, 11 Jun 2023 18:15:07 +0200 Subject: [PATCH 241/485] Change current directory only if necessary --- public/index.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/index.php b/public/index.php index d49672c6d1e7..a6d943a9d861 100644 --- a/public/index.php +++ b/public/index.php @@ -16,7 +16,9 @@ define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR); // Ensure the current directory is pointing to the front controller's directory -chdir(FCPATH); +if (getcwd() . DIRECTORY_SEPARATOR !== FCPATH) { + chdir(FCPATH); +} /* *--------------------------------------------------------------- From aba163e52b4498a4f3771784af7e666c5ef94fca Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Wed, 10 May 2023 00:11:26 -0500 Subject: [PATCH 242/485] feat: Hot Reloading --- app/Config/Events.php | 6 + app/Config/Toolbar.php | 27 + system/Debug/Toolbar/Views/toolbar.css | 880 ++++++++++---------- system/Debug/Toolbar/Views/toolbar.js | 799 ++++++++++-------- system/Debug/Toolbar/Views/toolbar.tpl.php | 5 + system/HotReloader/DirectoryHasher.php | 61 ++ system/HotReloader/HotReloader.php | 52 ++ system/HotReloader/IteratorFilter.php | 39 + user_guide_src/source/changelogs/v4.4.0.rst | 2 + user_guide_src/source/testing/debugging.rst | 11 + 10 files changed, 1098 insertions(+), 784 deletions(-) create mode 100644 system/HotReloader/DirectoryHasher.php create mode 100644 system/HotReloader/HotReloader.php create mode 100644 system/HotReloader/IteratorFilter.php diff --git a/app/Config/Events.php b/app/Config/Events.php index 5219f4ac3f68..bb48c66efde8 100644 --- a/app/Config/Events.php +++ b/app/Config/Events.php @@ -44,5 +44,11 @@ if (CI_DEBUG && ! is_cli()) { Events::on('DBQuery', 'CodeIgniter\Debug\Toolbar\Collectors\Database::collect'); Services::toolbar()->respond(); + // Hot Reload route - for framework use on the hot reloader. + if (ENVIRONMENT === 'development') { + Services::routes()->get('__hot-reload', function() { + (new \CodeIgniter\HotReloader\HotReloader())->run(); + }); + } } }); diff --git a/app/Config/Toolbar.php b/app/Config/Toolbar.php index ecab7a2cc1d3..38a42b59f1cf 100644 --- a/app/Config/Toolbar.php +++ b/app/Config/Toolbar.php @@ -88,4 +88,31 @@ class Toolbar extends BaseConfig * `$maxQueries` defines the maximum amount of queries that will be stored. */ public int $maxQueries = 100; + + /** + * -------------------------------------------------------------------------- + * Watched Directories + * -------------------------------------------------------------------------- + * + * Contains an array of directories that will be watched for changes and + * used to determine if the hot-reload feature should reload the page or not. + * We restrict the values to keep performance as high as possible. + * + * NOTE: The ROOTPATH will be prepended to all values. + */ + public array $watchedDirectories = [ + 'app', + ]; + + /** + * -------------------------------------------------------------------------- + * Watched File Extensions + * -------------------------------------------------------------------------- + * + * Contains an array of file extensions that will be watched for changes and + * used to determine if the hot-reload feature should reload the page or not. + */ + public array $watchedExtensions = [ + 'php', 'css', 'js', 'html', 'svg', 'json', 'env' + ]; } diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index 744d9392c2be..b8954dacfde3 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -7,321 +7,327 @@ * file that was distributed with this source code. */ #debug-icon { - bottom: 0; - position: fixed; - right: 0; - z-index: 10000; - height: 36px; - width: 36px; - margin: 0px; - padding: 0px; - clear: both; - text-align: center; + bottom: 0; + position: fixed; + right: 0; + z-index: 10000; + height: 36px; + width: 36px; + margin: 0px; + padding: 0px; + clear: both; + text-align: center; } #debug-icon a svg { - margin: 8px; - max-width: 20px; - max-height: 20px; + margin: 8px; + max-width: 20px; + max-height: 20px; } #debug-icon.fixed-top { - bottom: auto; - top: 0; + bottom: auto; + top: 0; } #debug-icon .debug-bar-ndisplay { - display: none; + display: none; } #debug-bar { - bottom: 0; - left: 0; - position: fixed; - right: 0; - z-index: 10000; - height: 36px; - line-height: 36px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; - font-size: 16px; - font-weight: 400; + bottom: 0; + left: 0; + position: fixed; + right: 0; + z-index: 10000; + height: 36px; + line-height: 36px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, + sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + font-size: 16px; + font-weight: 400; } #debug-bar h1 { - display: flex; - font-weight: normal; - margin: 0 0 0 auto; + display: flex; + font-weight: normal; + margin: 0 0 0 auto; } #debug-bar h1 svg { - width: 16px; - margin-right: 5px; + width: 16px; + margin-right: 5px; } #debug-bar h2 { - font-size: 16px; - margin: 0; - padding: 5px 0 10px 0; + font-size: 16px; + margin: 0; + padding: 5px 0 10px 0; } #debug-bar h2 span { - font-size: 13px; + font-size: 13px; } #debug-bar h3 { - font-size: 12px; - font-weight: 200; - margin: 0 0 0 10px; - padding: 0; - text-transform: uppercase; + font-size: 12px; + font-weight: 200; + margin: 0 0 0 10px; + padding: 0; + text-transform: uppercase; } #debug-bar p { - font-size: 12px; - margin: 0 0 0 15px; - padding: 0; + font-size: 12px; + margin: 0 0 0 15px; + padding: 0; } #debug-bar a { - text-decoration: none; + text-decoration: none; } #debug-bar a:hover { - text-decoration: underline; + text-decoration: underline; } #debug-bar button { - border: 1px solid; - border-radius: 4px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - cursor: pointer; - line-height: 15px; + border: 1px solid; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + cursor: pointer; + line-height: 15px; } #debug-bar button:hover { - text-decoration: underline; + text-decoration: underline; } #debug-bar table { - border-collapse: collapse; - font-size: 14px; - line-height: normal; - margin: 5px 10px 15px 10px; - width: calc(100% - 10px); + border-collapse: collapse; + font-size: 14px; + line-height: normal; + margin: 5px 10px 15px 10px; + width: calc(100% - 10px); } #debug-bar table strong { - font-weight: 500; + font-weight: 500; } #debug-bar table th { - display: table-cell; - font-weight: 600; - padding-bottom: 0.7em; - text-align: left; + display: table-cell; + font-weight: 600; + padding-bottom: 0.7em; + text-align: left; } #debug-bar table tr { - border: none; + border: none; } #debug-bar table td { - border: none; - display: table-cell; - margin: 0; - text-align: left; + border: none; + display: table-cell; + margin: 0; + text-align: left; } #debug-bar table td:first-child { - max-width: 20%; + max-width: 20%; } #debug-bar table td:first-child.narrow { - width: 7em; + width: 7em; } #debug-bar td[data-debugbar-route] form { - display: none; + display: none; } #debug-bar td[data-debugbar-route]:hover form { - display: block; + display: block; } #debug-bar td[data-debugbar-route]:hover > div { - display: none; + display: none; } -#debug-bar td[data-debugbar-route] input[type=text] { - padding: 2px; +#debug-bar td[data-debugbar-route] input[type="text"] { + padding: 2px; } #debug-bar .toolbar { - display: flex; - overflow: hidden; - overflow-y: auto; - padding: 0 12px 0 12px; - white-space: nowrap; - z-index: 10000; + display: flex; + overflow: hidden; + overflow-y: auto; + padding: 0 12px 0 12px; + white-space: nowrap; + z-index: 10000; } #debug-bar.fixed-top { - bottom: auto; - top: 0; + bottom: auto; + top: 0; } #debug-bar.fixed-top .tab { - bottom: auto; - top: 36px; + bottom: auto; + top: 36px; } #debug-bar #toolbar-position a, #debug-bar #toolbar-theme a { - padding: 0 6px; - display: inline-flex; - vertical-align: top; + padding: 0 6px; + display: inline-flex; + vertical-align: top; } #debug-bar #toolbar-position a:hover, #debug-bar #toolbar-theme a:hover { - text-decoration: none; + text-decoration: none; } #debug-bar #debug-bar-link { - display: flex; - padding: 6px; + display: flex; + padding: 6px; } #debug-bar .ci-label { - display: inline-flex; - font-size: 14px; + display: inline-flex; + font-size: 14px; } #debug-bar .ci-label:hover { - cursor: pointer; + cursor: pointer; } #debug-bar .ci-label a { - color: inherit; - display: flex; - letter-spacing: normal; - padding: 0 10px; - text-decoration: none; - align-items: center; + color: inherit; + display: flex; + letter-spacing: normal; + padding: 0 10px; + text-decoration: none; + align-items: center; } #debug-bar .ci-label img { - margin: 6px 3px 6px 0; - width: 16px !important; + margin: 6px 3px 6px 0; + width: 16px !important; } #debug-bar .ci-label .badge { - border-radius: 12px; - -moz-border-radius: 12px; - -webkit-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; + border-radius: 12px; + -moz-border-radius: 12px; + -webkit-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; } #debug-bar .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; + bottom: 35px; + display: none; + left: 0; + max-height: 62%; + overflow: hidden; + overflow-y: auto; + padding: 1em 2em; + position: fixed; + right: 0; + z-index: 9999; } #debug-bar .timeline { - margin-left: 0; - width: 100%; + margin-left: 0; + width: 100%; } #debug-bar .timeline th { - border-left: 1px solid; - font-size: 12px; - font-weight: 200; - padding: 5px 5px 10px 5px; - position: relative; - text-align: left; + border-left: 1px solid; + font-size: 12px; + font-weight: 200; + padding: 5px 5px 10px 5px; + position: relative; + text-align: left; } #debug-bar .timeline th:first-child { - border-left: 0; + border-left: 0; } #debug-bar .timeline td { - border-left: 1px solid; - padding: 5px; - position: relative; + border-left: 1px solid; + padding: 5px; + position: relative; } #debug-bar .timeline td:first-child { - border-left: 0; - max-width: none; + border-left: 0; + max-width: none; } #debug-bar .timeline td.child-container { - padding: 0px; + padding: 0px; } #debug-bar .timeline td.child-container .timeline { - margin: 0px; + 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 + 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; - -webkit-border-radius: 4px; - display: inline-block; - padding: 5px; - position: absolute; - top: 30%; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + display: inline-block; + padding: 5px; + position: absolute; + top: 30%; } #debug-bar .timeline .timeline-parent { - cursor: pointer; + 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; + 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; + background-color: #dfdfdf; } #debug-bar .timeline .timeline-parent-open td:first-child nav { - background-position: 0 75%; + background-position: 0 75%; } #debug-bar .timeline .child-row:hover { - background: transparent; + background: transparent; } #debug-bar .route-params, #debug-bar .route-params-item { - vertical-align: top; + vertical-align: top; } #debug-bar .route-params td:first-child, #debug-bar .route-params-item td:first-child { - font-style: italic; - padding-left: 1em; - text-align: right; + font-style: italic; + padding-left: 1em; + text-align: right; } .debug-view.show-view { - border: 1px solid; - margin: 4px; + border: 1px solid; + margin: 4px; } .debug-view-path { - font-family: monospace; - font-size: 12px; - letter-spacing: normal; - min-height: 16px; - padding: 2px; - text-align: left; + font-family: monospace; + font-size: 12px; + letter-spacing: normal; + min-height: 16px; + padding: 2px; + text-align: left; } .show-view .debug-view-path { - display: block !important; + display: block !important; } @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; + } } #debug-icon { - background-color: #FFFFFF; - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; + background-color: #ffffff; + 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 { - color: #DD8615; + color: #dd8615; } #debug-bar { - background-color: #FFFFFF; - color: #434343; + background-color: #ffffff; + color: #434343; } #debug-bar h1, #debug-bar h2, @@ -335,217 +341,217 @@ #debug-bar td, #debug-bar button, #debug-bar .toolbar { - background-color: transparent; - color: #434343; + background-color: transparent; + color: #434343; } #debug-bar button { - background-color: #FFFFFF; + background-color: #ffffff; } #debug-bar table strong { - color: #DD8615; + color: #dd8615; } #debug-bar table tbody tr:hover { - background-color: #DFDFDF; + background-color: #dfdfdf; } #debug-bar table tbody tr.current { - background-color: #FDC894; + background-color: #fdc894; } #debug-bar table tbody tr.current:hover td { - background-color: #DD4814; - color: #FFFFFF; + background-color: #dd4814; + color: #ffffff; } #debug-bar .toolbar { - background-color: #FFFFFF; - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; + background-color: #ffffff; + box-shadow: 0 0 4px #dfdfdf; + -moz-box-shadow: 0 0 4px #dfdfdf; + -webkit-box-shadow: 0 0 4px #dfdfdf; } #debug-bar .toolbar img { - filter: brightness(0) invert(0.4); + filter: brightness(0) invert(0.4); } #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; + box-shadow: 0 0 4px #dfdfdf; + -moz-box-shadow: 0 0 4px #dfdfdf; + -webkit-box-shadow: 0 0 4px #dfdfdf; } #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #DFDFDF; - -moz-box-shadow: 0 1px 4px #DFDFDF; - -webkit-box-shadow: 0 1px 4px #DFDFDF; + box-shadow: 0 1px 4px #dfdfdf; + -moz-box-shadow: 0 1px 4px #dfdfdf; + -webkit-box-shadow: 0 1px 4px #dfdfdf; } #debug-bar .muted { - color: #434343; + color: #434343; } #debug-bar .muted td { - color: #DFDFDF; + color: #dfdfdf; } #debug-bar .muted:hover td { - color: #434343; + color: #434343; } #debug-bar #toolbar-position, #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); + filter: brightness(0) invert(0.6); } #debug-bar .ci-label.active { - background-color: #DFDFDF; + background-color: #dfdfdf; } #debug-bar .ci-label:hover { - background-color: #DFDFDF; + background-color: #dfdfdf; } #debug-bar .ci-label .badge { - background-color: #DD4814; - color: #FFFFFF; + background-color: #dd4814; + color: #ffffff; } #debug-bar .tab { - background-color: #FFFFFF; - box-shadow: 0 -1px 4px #DFDFDF; - -moz-box-shadow: 0 -1px 4px #DFDFDF; - -webkit-box-shadow: 0 -1px 4px #DFDFDF; + background-color: #ffffff; + box-shadow: 0 -1px 4px #dfdfdf; + -moz-box-shadow: 0 -1px 4px #dfdfdf; + -webkit-box-shadow: 0 -1px 4px #dfdfdf; } #debug-bar .timeline th, #debug-bar .timeline td { - border-color: #DFDFDF; + border-color: #dfdfdf; } #debug-bar .timeline .timer { - background-color: #DD8615; + background-color: #dd8615; } .debug-view.show-view { - border-color: #DD8615; + border-color: #dd8615; } .debug-view-path { - background-color: #FDC894; - color: #434343; + background-color: #fdc894; + color: #434343; } @media (prefers-color-scheme: dark) { - #debug-icon { - background-color: #252525; - 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 { - color: #DD8615; - } - #debug-bar { - background-color: #252525; - color: #DFDFDF; - } - #debug-bar h1, - #debug-bar h2, - #debug-bar h3, - #debug-bar p, - #debug-bar a, - #debug-bar button, - #debug-bar table, - #debug-bar thead, - #debug-bar tr, - #debug-bar td, - #debug-bar button, - #debug-bar .toolbar { - background-color: transparent; - color: #DFDFDF; - } - #debug-bar button { - background-color: #252525; - } - #debug-bar table strong { - color: #DD8615; - } - #debug-bar table tbody tr:hover { - background-color: #434343; - } - #debug-bar table tbody tr.current { - background-color: #FDC894; - } - #debug-bar table tbody tr.current td { - color: #252525; - } - #debug-bar table tbody tr.current:hover td { - background-color: #DD4814; - color: #FFFFFF; - } - #debug-bar .toolbar { - background-color: #434343; - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; - } - #debug-bar .toolbar img { - filter: brightness(0) invert(1); - } - #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; - } - #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #434343; - -moz-box-shadow: 0 1px 4px #434343; - -webkit-box-shadow: 0 1px 4px #434343; - } - #debug-bar .muted { - color: #DFDFDF; - } - #debug-bar .muted td { - color: #434343; - } - #debug-bar .muted:hover td { - color: #DFDFDF; - } - #debug-bar #toolbar-position, - #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); - } - #debug-bar .ci-label.active { - background-color: #252525; - } - #debug-bar .ci-label:hover { - background-color: #252525; - } - #debug-bar .ci-label .badge { - background-color: #DD4814; - color: #FFFFFF; - } - #debug-bar .tab { - background-color: #252525; - box-shadow: 0 -1px 4px #434343; - -moz-box-shadow: 0 -1px 4px #434343; - -webkit-box-shadow: 0 -1px 4px #434343; - } - #debug-bar .timeline th, - #debug-bar .timeline td { - border-color: #434343; - } - #debug-bar .timeline .timer { - background-color: #DD8615; - } - .debug-view.show-view { - border-color: #DD8615; - } - .debug-view-path { - background-color: #FDC894; - color: #434343; - } + #debug-icon { + background-color: #252525; + 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 { + color: #dd8615; + } + #debug-bar { + background-color: #252525; + color: #dfdfdf; + } + #debug-bar h1, + #debug-bar h2, + #debug-bar h3, + #debug-bar p, + #debug-bar a, + #debug-bar button, + #debug-bar table, + #debug-bar thead, + #debug-bar tr, + #debug-bar td, + #debug-bar button, + #debug-bar .toolbar { + background-color: transparent; + color: #dfdfdf; + } + #debug-bar button { + background-color: #252525; + } + #debug-bar table strong { + color: #dd8615; + } + #debug-bar table tbody tr:hover { + background-color: #434343; + } + #debug-bar table tbody tr.current { + background-color: #fdc894; + } + #debug-bar table tbody tr.current td { + color: #252525; + } + #debug-bar table tbody tr.current:hover td { + background-color: #dd4814; + color: #ffffff; + } + #debug-bar .toolbar { + background-color: #434343; + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; + } + #debug-bar .toolbar img { + filter: brightness(0) invert(1); + } + #debug-bar.fixed-top .toolbar { + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; + } + #debug-bar.fixed-top .tab { + box-shadow: 0 1px 4px #434343; + -moz-box-shadow: 0 1px 4px #434343; + -webkit-box-shadow: 0 1px 4px #434343; + } + #debug-bar .muted { + color: #dfdfdf; + } + #debug-bar .muted td { + color: #434343; + } + #debug-bar .muted:hover td { + color: #dfdfdf; + } + #debug-bar #toolbar-position, + #debug-bar #toolbar-theme { + filter: brightness(0) invert(0.6); + } + #debug-bar .ci-label.active { + background-color: #252525; + } + #debug-bar .ci-label:hover { + background-color: #252525; + } + #debug-bar .ci-label .badge { + background-color: #dd4814; + color: #ffffff; + } + #debug-bar .tab { + background-color: #252525; + box-shadow: 0 -1px 4px #434343; + -moz-box-shadow: 0 -1px 4px #434343; + -webkit-box-shadow: 0 -1px 4px #434343; + } + #debug-bar .timeline th, + #debug-bar .timeline td { + border-color: #434343; + } + #debug-bar .timeline .timer { + background-color: #dd8615; + } + .debug-view.show-view { + border-color: #dd8615; + } + .debug-view-path { + background-color: #fdc894; + color: #434343; + } } #toolbarContainer.dark #debug-icon { - background-color: #252525; - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; + background-color: #252525; + 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 { - color: #DD8615; + color: #dd8615; } #toolbarContainer.dark #debug-bar { - background-color: #252525; - color: #DFDFDF; + background-color: #252525; + color: #dfdfdf; } #toolbarContainer.dark #debug-bar h1, #toolbarContainer.dark #debug-bar h2, @@ -559,109 +565,109 @@ #toolbarContainer.dark #debug-bar td, #toolbarContainer.dark #debug-bar button, #toolbarContainer.dark #debug-bar .toolbar { - background-color: transparent; - color: #DFDFDF; + background-color: transparent; + color: #dfdfdf; } #toolbarContainer.dark #debug-bar button { - background-color: #252525; + background-color: #252525; } #toolbarContainer.dark #debug-bar table strong { - color: #DD8615; + color: #dd8615; } #toolbarContainer.dark #debug-bar table tbody tr:hover { - background-color: #434343; + background-color: #434343; } #toolbarContainer.dark #debug-bar table tbody tr.current { - background-color: #FDC894; + background-color: #fdc894; } #toolbarContainer.dark #debug-bar table tbody tr.current td { - color: #252525; + color: #252525; } #toolbarContainer.dark #debug-bar table tbody tr.current:hover td { - background-color: #DD4814; - color: #FFFFFF; + background-color: #dd4814; + color: #ffffff; } #toolbarContainer.dark #debug-bar .toolbar { - background-color: #434343; - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; + background-color: #434343; + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; } #toolbarContainer.dark #debug-bar .toolbar img { - filter: brightness(0) invert(1); + filter: brightness(0) invert(1); } #toolbarContainer.dark #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; } #toolbarContainer.dark #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #434343; - -moz-box-shadow: 0 1px 4px #434343; - -webkit-box-shadow: 0 1px 4px #434343; + box-shadow: 0 1px 4px #434343; + -moz-box-shadow: 0 1px 4px #434343; + -webkit-box-shadow: 0 1px 4px #434343; } #toolbarContainer.dark #debug-bar .muted { - color: #DFDFDF; + color: #dfdfdf; } #toolbarContainer.dark #debug-bar .muted td { - color: #434343; + color: #434343; } #toolbarContainer.dark #debug-bar .muted:hover td { - color: #DFDFDF; + color: #dfdfdf; } #toolbarContainer.dark #debug-bar #toolbar-position, #toolbarContainer.dark #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); + filter: brightness(0) invert(0.6); } #toolbarContainer.dark #debug-bar .ci-label.active { - background-color: #252525; + background-color: #252525; } #toolbarContainer.dark #debug-bar .ci-label:hover { - background-color: #252525; + background-color: #252525; } #toolbarContainer.dark #debug-bar .ci-label .badge { - background-color: #DD4814; - color: #FFFFFF; + background-color: #dd4814; + color: #ffffff; } #toolbarContainer.dark #debug-bar .tab { - background-color: #252525; - box-shadow: 0 -1px 4px #434343; - -moz-box-shadow: 0 -1px 4px #434343; - -webkit-box-shadow: 0 -1px 4px #434343; + background-color: #252525; + box-shadow: 0 -1px 4px #434343; + -moz-box-shadow: 0 -1px 4px #434343; + -webkit-box-shadow: 0 -1px 4px #434343; } #toolbarContainer.dark #debug-bar .timeline th, #toolbarContainer.dark #debug-bar .timeline td { - border-color: #434343; + border-color: #434343; } #toolbarContainer.dark #debug-bar .timeline .timer { - background-color: #DD8615; + background-color: #dd8615; } #toolbarContainer.dark .debug-view.show-view { - border-color: #DD8615; + border-color: #dd8615; } #toolbarContainer.dark .debug-view-path { - background-color: #FDC894; - color: #434343; + background-color: #fdc894; + color: #434343; } -#toolbarContainer.dark td[data-debugbar-route] input[type=text] { - background: #000; - color: #fff; +#toolbarContainer.dark td[data-debugbar-route] input[type="text"] { + background: #000; + color: #fff; } #toolbarContainer.light #debug-icon { - background-color: #FFFFFF; - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; + background-color: #ffffff; + 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 { - color: #DD8615; + color: #dd8615; } #toolbarContainer.light #debug-bar { - background-color: #FFFFFF; - color: #434343; + background-color: #ffffff; + color: #434343; } #toolbarContainer.light #debug-bar h1, #toolbarContainer.light #debug-bar h2, @@ -675,124 +681,134 @@ #toolbarContainer.light #debug-bar td, #toolbarContainer.light #debug-bar button, #toolbarContainer.light #debug-bar .toolbar { - background-color: transparent; - color: #434343; + background-color: transparent; + color: #434343; } #toolbarContainer.light #debug-bar button { - background-color: #FFFFFF; + background-color: #ffffff; } #toolbarContainer.light #debug-bar table strong { - color: #DD8615; + color: #dd8615; } #toolbarContainer.light #debug-bar table tbody tr:hover { - background-color: #DFDFDF; + background-color: #dfdfdf; } #toolbarContainer.light #debug-bar table tbody tr.current { - background-color: #FDC894; + background-color: #fdc894; } #toolbarContainer.light #debug-bar table tbody tr.current:hover td { - background-color: #DD4814; - color: #FFFFFF; + background-color: #dd4814; + color: #ffffff; } #toolbarContainer.light #debug-bar .toolbar { - background-color: #FFFFFF; - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; + background-color: #ffffff; + box-shadow: 0 0 4px #dfdfdf; + -moz-box-shadow: 0 0 4px #dfdfdf; + -webkit-box-shadow: 0 0 4px #dfdfdf; } #toolbarContainer.light #debug-bar .toolbar img { - filter: brightness(0) invert(0.4); + filter: brightness(0) invert(0.4); } #toolbarContainer.light #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; + box-shadow: 0 0 4px #dfdfdf; + -moz-box-shadow: 0 0 4px #dfdfdf; + -webkit-box-shadow: 0 0 4px #dfdfdf; } #toolbarContainer.light #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #DFDFDF; - -moz-box-shadow: 0 1px 4px #DFDFDF; - -webkit-box-shadow: 0 1px 4px #DFDFDF; + box-shadow: 0 1px 4px #dfdfdf; + -moz-box-shadow: 0 1px 4px #dfdfdf; + -webkit-box-shadow: 0 1px 4px #dfdfdf; } #toolbarContainer.light #debug-bar .muted { - color: #434343; + color: #434343; } #toolbarContainer.light #debug-bar .muted td { - color: #DFDFDF; + color: #dfdfdf; } #toolbarContainer.light #debug-bar .muted:hover td { - color: #434343; + color: #434343; } #toolbarContainer.light #debug-bar #toolbar-position, #toolbarContainer.light #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); + filter: brightness(0) invert(0.6); } #toolbarContainer.light #debug-bar .ci-label.active { - background-color: #DFDFDF; + background-color: #dfdfdf; } #toolbarContainer.light #debug-bar .ci-label:hover { - background-color: #DFDFDF; + background-color: #dfdfdf; } #toolbarContainer.light #debug-bar .ci-label .badge { - background-color: #DD4814; - color: #FFFFFF; + background-color: #dd4814; + color: #ffffff; } #toolbarContainer.light #debug-bar .tab { - background-color: #FFFFFF; - box-shadow: 0 -1px 4px #DFDFDF; - -moz-box-shadow: 0 -1px 4px #DFDFDF; - -webkit-box-shadow: 0 -1px 4px #DFDFDF; + background-color: #ffffff; + box-shadow: 0 -1px 4px #dfdfdf; + -moz-box-shadow: 0 -1px 4px #dfdfdf; + -webkit-box-shadow: 0 -1px 4px #dfdfdf; } #toolbarContainer.light #debug-bar .timeline th, #toolbarContainer.light #debug-bar .timeline td { - border-color: #DFDFDF; + border-color: #dfdfdf; } #toolbarContainer.light #debug-bar .timeline .timer { - background-color: #DD8615; + background-color: #dd8615; } #toolbarContainer.light .debug-view.show-view { - border-color: #DD8615; + border-color: #dd8615; } #toolbarContainer.light .debug-view-path { - background-color: #FDC894; - color: #434343; + background-color: #fdc894; + color: #434343; } .debug-bar-width30 { - width: 30%; + width: 30%; } .debug-bar-width10 { - width: 10%; + width: 10%; } .debug-bar-width70p { - width: 70px; + width: 70px; } .debug-bar-width190p { - width: 190px; + width: 190px; } .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; + overflow: hidden; +} + +/* ENDLESS ROTATE */ +.rotate { + animation: rotate 9s linear infinite; +} +@keyframes rotate { + to { + transform: rotate(360deg); + } } diff --git a/system/Debug/Toolbar/Views/toolbar.js b/system/Debug/Toolbar/Views/toolbar.js index 7805a99dda05..bfff574507e7 100644 --- a/system/Debug/Toolbar/Views/toolbar.js +++ b/system/Debug/Toolbar/Views/toolbar.js @@ -3,15 +3,14 @@ */ var ciDebugBar = { + toolbarContainer: null, + toolbar: null, + icon: null, - toolbarContainer : null, - toolbar : null, - icon : null, - - init : function () { - this.toolbarContainer = document.getElementById('toolbarContainer'); - this.toolbar = document.getElementById('debug-bar'); - this.icon = document.getElementById('debug-icon'); + init: function () { + this.toolbarContainer = document.getElementById("toolbarContainer"); + this.toolbar = document.getElementById("debug-bar"); + this.icon = document.getElementById("debug-icon"); ciDebugBar.createListeners(); ciDebugBar.setToolbarState(); @@ -19,115 +18,123 @@ var ciDebugBar = { ciDebugBar.setToolbarTheme(); ciDebugBar.toggleViewsHints(); ciDebugBar.routerLink(); + ciDebugBar.setHotReloadState(); - document.getElementById('debug-bar-link').addEventListener('click', ciDebugBar.toggleToolbar, true); - document.getElementById('debug-icon-link').addEventListener('click', ciDebugBar.toggleToolbar, true); + document + .getElementById("debug-bar-link") + .addEventListener("click", ciDebugBar.toggleToolbar, true); + document + .getElementById("debug-icon-link") + .addEventListener("click", ciDebugBar.toggleToolbar, true); // Allows to highlight the row of the current history request - var btn = this.toolbar.querySelector('button[data-time="' + localStorage.getItem('debugbar-time') + '"]'); - ciDebugBar.addClass(btn.parentNode.parentNode, 'current'); - - historyLoad = this.toolbar.getElementsByClassName('ci-history-load'); - - for (var i = 0; i < historyLoad.length; i++) - { - historyLoad[i].addEventListener('click', function () { - loadDoc(this.getAttribute('data-time')); - }, true); + var btn = this.toolbar.querySelector( + 'button[data-time="' + localStorage.getItem("debugbar-time") + '"]' + ); + ciDebugBar.addClass(btn.parentNode.parentNode, "current"); + + historyLoad = this.toolbar.getElementsByClassName("ci-history-load"); + + for (var i = 0; i < historyLoad.length; i++) { + historyLoad[i].addEventListener( + "click", + function () { + loadDoc(this.getAttribute("data-time")); + }, + true + ); } // Display the active Tab on page load - var tab = ciDebugBar.readCookie('debug-bar-tab'); - if (document.getElementById(tab)) - { - var el = document.getElementById(tab); - el.style.display = 'block'; - ciDebugBar.addClass(el, 'active'); - tab = document.querySelector('[data-tab=' + tab + ']'); - if (tab) - { - ciDebugBar.addClass(tab.parentNode, 'active'); + var tab = ciDebugBar.readCookie("debug-bar-tab"); + if (document.getElementById(tab)) { + var el = document.getElementById(tab); + el.style.display = "block"; + ciDebugBar.addClass(el, "active"); + tab = document.querySelector("[data-tab=" + tab + "]"); + if (tab) { + ciDebugBar.addClass(tab.parentNode, "active"); } } }, - createListeners : function () { - var buttons = [].slice.call(this.toolbar.querySelectorAll('.ci-label a')); + createListeners: function () { + var buttons = [].slice.call( + this.toolbar.querySelectorAll(".ci-label a") + ); - for (var i = 0; i < buttons.length; i++) - { - buttons[i].addEventListener('click', ciDebugBar.showTab, true); + for (var i = 0; i < buttons.length; i++) { + buttons[i].addEventListener("click", ciDebugBar.showTab, true); } // Hook up generic toggle via data attributes `data-toggle="foo"` - var links = this.toolbar.querySelectorAll('[data-toggle]'); - for (var i = 0; i < links.length; i++) - { - links[i].addEventListener('click', ciDebugBar.toggleRows, true); + var links = this.toolbar.querySelectorAll("[data-toggle]"); + for (var i = 0; i < links.length; i++) { + links[i].addEventListener("click", ciDebugBar.toggleRows, true); } }, showTab: function () { // Get the target tab, if any - var tab = document.getElementById(this.getAttribute('data-tab')); + var tab = document.getElementById(this.getAttribute("data-tab")); // If the label have not a tab stops here - if (! tab) - { + if (!tab) { return; } // Remove debug-bar-tab cookie - ciDebugBar.createCookie('debug-bar-tab', '', -1); + ciDebugBar.createCookie("debug-bar-tab", "", -1); // Check our current state. var state = tab.style.display; // Hide all tabs - var tabs = document.querySelectorAll('#debug-bar .tab'); + var tabs = document.querySelectorAll("#debug-bar .tab"); - for (var i = 0; i < tabs.length; i++) - { - tabs[i].style.display = 'none'; + for (var i = 0; i < tabs.length; i++) { + tabs[i].style.display = "none"; } // Mark all labels as inactive - var labels = document.querySelectorAll('#debug-bar .ci-label'); + var labels = document.querySelectorAll("#debug-bar .ci-label"); - for (var i = 0; i < labels.length; i++) - { - ciDebugBar.removeClass(labels[i], 'active'); + for (var i = 0; i < labels.length; i++) { + ciDebugBar.removeClass(labels[i], "active"); } // Show/hide the selected tab - if (state != 'block') - { - tab.style.display = 'block'; - ciDebugBar.addClass(this.parentNode, 'active'); + if (state != "block") { + tab.style.display = "block"; + ciDebugBar.addClass(this.parentNode, "active"); // Create debug-bar-tab cookie to persistent state - ciDebugBar.createCookie('debug-bar-tab', this.getAttribute('data-tab'), 365); + ciDebugBar.createCookie( + "debug-bar-tab", + this.getAttribute("data-tab"), + 365 + ); } }, - addClass : function (el, className) { - if (el.classList) - { + addClass: function (el, className) { + if (el.classList) { el.classList.add(className); - } - else - { - el.className += ' ' + className; + } else { + el.className += " " + className; } }, - removeClass : function (el, className) { - if (el.classList) - { + removeClass: function (el, className) { + if (el.classList) { el.classList.remove(className); - } - else - { - el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); + } else { + el.className = el.className.replace( + new RegExp( + "(^|\\b)" + className.split(" ").join("|") + "(\\b|$)", + "gi" + ), + " " + ); } }, @@ -137,12 +144,14 @@ var ciDebugBar = { * * @param event */ - toggleRows : function(event) { - if(event.target) - { - let row = event.target.closest('tr'); - let target = document.getElementById(row.getAttribute('data-toggle')); - target.style.display = target.style.display === 'none' ? 'table-row' : 'none'; + toggleRows: function (event) { + if (event.target) { + let row = event.target.closest("tr"); + let target = document.getElementById( + row.getAttribute("data-toggle") + ); + target.style.display = + target.style.display === "none" ? "table-row" : "none"; } }, @@ -151,15 +160,13 @@ var ciDebugBar = { * * @param obj */ - toggleDataTable : function (obj) { - if (typeof obj == 'string') - { - obj = document.getElementById(obj + '_table'); + toggleDataTable: function (obj) { + if (typeof obj == "string") { + obj = document.getElementById(obj + "_table"); } - if (obj) - { - obj.style.display = obj.style.display === 'none' ? 'block' : 'none'; + if (obj) { + obj.style.display = obj.style.display === "none" ? "block" : "none"; } }, @@ -168,35 +175,37 @@ var ciDebugBar = { * * @param obj */ - toggleChildRows : function (obj) { - if (typeof obj == 'string') - { - par = document.getElementById(obj + '_parent') - obj = document.getElementById(obj + '_children'); + 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'); + 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 */ - toggleToolbar : function () { - var open = ciDebugBar.toolbar.style.display != 'none'; + toggleToolbar: function () { + var open = ciDebugBar.toolbar.style.display != "none"; - ciDebugBar.icon.style.display = open == true ? 'inline-block' : 'none'; - ciDebugBar.toolbar.style.display = open == false ? 'inline-block' : 'none'; + ciDebugBar.icon.style.display = open == true ? "inline-block" : "none"; + ciDebugBar.toolbar.style.display = + open == false ? "inline-block" : "none"; // Remember it for other page loads on this site - ciDebugBar.createCookie('debug-bar-state', '', -1); - ciDebugBar.createCookie('debug-bar-state', open == true ? 'minimized' : 'open' , 365); + ciDebugBar.createCookie("debug-bar-state", "", -1); + ciDebugBar.createCookie( + "debug-bar-state", + open == true ? "minimized" : "open", + 365 + ); }, /** @@ -204,49 +213,58 @@ var ciDebugBar = { * the page is first loaded to allow it to remember the state between refreshes. */ setToolbarState: function () { - var open = ciDebugBar.readCookie('debug-bar-state'); + var open = ciDebugBar.readCookie("debug-bar-state"); - ciDebugBar.icon.style.display = open != 'open' ? 'inline-block' : 'none'; - ciDebugBar.toolbar.style.display = open == 'open' ? 'inline-block' : 'none'; + ciDebugBar.icon.style.display = + open != "open" ? "inline-block" : "none"; + ciDebugBar.toolbar.style.display = + open == "open" ? "inline-block" : "none"; }, toggleViewsHints: function () { // Avoid toggle hints on history requests that are not the initial - if (localStorage.getItem('debugbar-time') != localStorage.getItem('debugbar-time-new')) - { - var a = document.querySelector('a[data-tab="ci-views"]'); - a.href = '#'; + if ( + localStorage.getItem("debugbar-time") != + localStorage.getItem("debugbar-time-new") + ) { + var a = document.querySelector('a[data-tab="ci-views"]'); + a.href = "#"; return; } - var nodeList = []; // [ Element, NewElement( 1 )/OldElement( 0 ) ] + var nodeList = []; // [ Element, NewElement( 1 )/OldElement( 0 ) ] var sortedComments = []; - var comments = []; + var comments = []; var getComments = function () { - var nodes = []; - var result = []; - var xpathResults = document.evaluate( "//comment()[starts-with(., ' DEBUG-VIEW')]", document, null, XPathResult.ANY_TYPE, null); - var nextNode = xpathResults.iterateNext(); - while ( nextNode ) - { - nodes.push( nextNode ); + var nodes = []; + var result = []; + var xpathResults = document.evaluate( + "//comment()[starts-with(., ' DEBUG-VIEW')]", + document, + null, + XPathResult.ANY_TYPE, + null + ); + var nextNode = xpathResults.iterateNext(); + while (nextNode) { + nodes.push(nextNode); nextNode = xpathResults.iterateNext(); } // sort comment by opening and closing tags - for (var i = 0; i < nodes.length; ++i) - { + for (var i = 0; i < nodes.length; ++i) { // get file path + name to use as key - var path = nodes[i].nodeValue.substring( 18, nodes[i].nodeValue.length - 1 ); + var path = nodes[i].nodeValue.substring( + 18, + nodes[i].nodeValue.length - 1 + ); - if ( nodes[i].nodeValue[12] === 'S' ) // simple check for start comment - { + if (nodes[i].nodeValue[12] === "S") { + // simple check for start comment // create new entry - result[path] = [ nodes[i], null ]; - } - else if (result[path]) - { + result[path] = [nodes[i], null]; + } else if (result[path]) { // add to existing entry result[path][1] = nodes[i]; } @@ -256,73 +274,81 @@ var ciDebugBar = { }; // find node that has TargetNode as parentNode - var getParentNode = function ( node, targetNode ) { - if ( node.parentNode === null ) - { + var getParentNode = function (node, targetNode) { + if (node.parentNode === null) { return null; } - if ( node.parentNode !== targetNode ) - { - return getParentNode( node.parentNode, targetNode ); + if (node.parentNode !== targetNode) { + return getParentNode(node.parentNode, targetNode); } return node; }; // define invalid & outer ( also invalid ) elements - const INVALID_ELEMENTS = [ 'NOSCRIPT', 'SCRIPT', 'STYLE' ]; - const OUTER_ELEMENTS = [ 'HTML', 'BODY', 'HEAD' ]; + const INVALID_ELEMENTS = ["NOSCRIPT", "SCRIPT", "STYLE"]; + const OUTER_ELEMENTS = ["HTML", "BODY", "HEAD"]; - var getValidElementInner = function ( node, reverse ) { + var getValidElementInner = function (node, reverse) { // handle invalid tags - if ( OUTER_ELEMENTS.indexOf( node.nodeName ) !== -1 ) - { - for (var i = 0; i < document.body.children.length; ++i) - { - var index = reverse ? document.body.children.length - ( i + 1 ) : i; + if (OUTER_ELEMENTS.indexOf(node.nodeName) !== -1) { + for (var i = 0; i < document.body.children.length; ++i) { + var index = reverse + ? document.body.children.length - (i + 1) + : i; var element = document.body.children[index]; // skip invalid tags - if ( INVALID_ELEMENTS.indexOf( element.nodeName ) !== -1 ) - { + if (INVALID_ELEMENTS.indexOf(element.nodeName) !== -1) { continue; } - return [ element, reverse ]; + return [element, reverse]; } return null; } // get to next valid element - while ( node !== null && INVALID_ELEMENTS.indexOf( node.nodeName ) !== -1 ) - { - node = reverse ? node.previousElementSibling : node.nextElementSibling; + while ( + node !== null && + INVALID_ELEMENTS.indexOf(node.nodeName) !== -1 + ) { + node = reverse + ? node.previousElementSibling + : node.nextElementSibling; } // return non array if we couldnt find something - if ( node === null ) - { + if (node === null) { return null; } - return [ node, reverse ]; + return [node, reverse]; }; // get next valid element ( to be safe to add divs ) // @return [ element, skip element ] or null if we couldnt find a valid place - var getValidElement = function ( nodeElement ) { - if (nodeElement) - { - if ( nodeElement.nextElementSibling !== null ) - { - return getValidElementInner( nodeElement.nextElementSibling, false ) - || getValidElementInner( nodeElement.previousElementSibling, true ); + var getValidElement = function (nodeElement) { + if (nodeElement) { + if (nodeElement.nextElementSibling !== null) { + return ( + getValidElementInner( + nodeElement.nextElementSibling, + false + ) || + getValidElementInner( + nodeElement.previousElementSibling, + true + ) + ); } - if ( nodeElement.previousElementSibling !== null ) - { - return getValidElementInner( nodeElement.previousElementSibling, true ); + if (nodeElement.previousElementSibling !== null) { + return getValidElementInner( + nodeElement.previousElementSibling, + true + ); } } @@ -330,89 +356,80 @@ var ciDebugBar = { return null; }; - function showHints() - { + function showHints() { // Had AJAX? Reset view blocks sortedComments = getComments(); - for (var key in sortedComments) - { - var startElement = getValidElement( sortedComments[key][0] ); - var endElement = getValidElement( sortedComments[key][1] ); + for (var key in sortedComments) { + var startElement = getValidElement(sortedComments[key][0]); + var endElement = getValidElement(sortedComments[key][1]); // skip if we couldnt get a valid element - if ( startElement === null || endElement === null ) - { + if (startElement === null || endElement === null) { continue; } // find element which has same parent as startelement - var jointParent = getParentNode( endElement[0], startElement[0].parentNode ); - if ( jointParent === null ) - { + var jointParent = getParentNode( + endElement[0], + startElement[0].parentNode + ); + if (jointParent === null) { // find element which has same parent as endelement - jointParent = getParentNode( startElement[0], endElement[0].parentNode ); - if ( jointParent === null ) - { + jointParent = getParentNode( + startElement[0], + endElement[0].parentNode + ); + if (jointParent === null) { // both tries failed continue; - } - else - { + } else { startElement[0] = jointParent; } - } - else - { + } else { endElement[0] = jointParent; } - var debugDiv = document.createElement( 'div' ); // holder - var debugPath = document.createElement( 'div' ); // path + var debugDiv = document.createElement("div"); // holder + var debugPath = document.createElement("div"); // path var childArray = startElement[0].parentNode.childNodes; // target child array - var parent = startElement[0].parentNode; + var parent = startElement[0].parentNode; var start, end; // setup container - debugDiv.classList.add( 'debug-view' ); - debugDiv.classList.add( 'show-view' ); - debugPath.classList.add( 'debug-view-path' ); + debugDiv.classList.add("debug-view"); + debugDiv.classList.add("show-view"); + debugPath.classList.add("debug-view-path"); debugPath.innerText = key; - debugDiv.appendChild( debugPath ); + debugDiv.appendChild(debugPath); // calc distance between them // start - for (var i = 0; i < childArray.length; ++i) - { + for (var i = 0; i < childArray.length; ++i) { // check for comment ( start & end ) -> if its before valid start element - if ( childArray[i] === sortedComments[key][1] || + if ( + childArray[i] === sortedComments[key][1] || childArray[i] === sortedComments[key][0] || - childArray[i] === startElement[0] ) - { + childArray[i] === startElement[0] + ) { start = i; - if ( childArray[i] === sortedComments[key][0] ) - { + if (childArray[i] === sortedComments[key][0]) { start++; // increase to skip the start comment } break; } } // adjust if we want to skip the start element - if ( startElement[1] ) - { + if (startElement[1]) { start++; } // end - for (var i = start; i < childArray.length; ++i) - { - if ( childArray[i] === endElement[0] ) - { + for (var i = start; i < childArray.length; ++i) { + if (childArray[i] === endElement[0]) { end = i; // dont break to check for end comment after end valid element - } - else if ( childArray[i] === sortedComments[key][1] ) - { + } else if (childArray[i] === sortedComments[key][1]) { // if we found the end comment, we can break end = i; break; @@ -421,161 +438,230 @@ var ciDebugBar = { // move elements var number = end - start; - if ( endElement[1] ) - { + if (endElement[1]) { number++; } - for (var i = 0; i < number; ++i) - { - if ( INVALID_ELEMENTS.indexOf( childArray[start] ) !== -1 ) - { + for (var i = 0; i < number; ++i) { + if (INVALID_ELEMENTS.indexOf(childArray[start]) !== -1) { // skip invalid childs that can cause problems if moved start++; continue; } - debugDiv.appendChild( childArray[start] ); + debugDiv.appendChild(childArray[start]); } // add container to DOM - nodeList.push( parent.insertBefore( debugDiv, childArray[start] ) ); + nodeList.push(parent.insertBefore(debugDiv, childArray[start])); } - ciDebugBar.createCookie('debug-view', 'show', 365); - ciDebugBar.addClass(btn, 'active'); + ciDebugBar.createCookie("debug-view", "show", 365); + ciDebugBar.addClass(btn, "active"); } - function hideHints() - { - for (var i = 0; i < nodeList.length; ++i) - { + function hideHints() { + for (var i = 0; i < nodeList.length; ++i) { var index; // find index - for (var j = 0; j < nodeList[i].parentNode.childNodes.length; ++j) - { - if ( nodeList[i].parentNode.childNodes[j] === nodeList[i] ) - { + for ( + var j = 0; + j < nodeList[i].parentNode.childNodes.length; + ++j + ) { + if (nodeList[i].parentNode.childNodes[j] === nodeList[i]) { index = j; break; } } // move child back - while ( nodeList[i].childNodes.length !== 1 ) - { - nodeList[i].parentNode.insertBefore( nodeList[i].childNodes[1], nodeList[i].parentNode.childNodes[index].nextSibling ); + while (nodeList[i].childNodes.length !== 1) { + nodeList[i].parentNode.insertBefore( + nodeList[i].childNodes[1], + nodeList[i].parentNode.childNodes[index].nextSibling + ); index++; } - nodeList[i].parentNode.removeChild( nodeList[i] ); + nodeList[i].parentNode.removeChild(nodeList[i]); } nodeList.length = 0; - ciDebugBar.createCookie('debug-view', '', -1); - ciDebugBar.removeClass(btn, 'active'); + ciDebugBar.createCookie("debug-view", "", -1); + ciDebugBar.removeClass(btn, "active"); } - var btn = document.querySelector('[data-tab=ci-views]'); + var btn = document.querySelector("[data-tab=ci-views]"); // If the Views Collector is inactive stops here - if (! btn) - { + if (!btn) { return; } btn.parentNode.onclick = function () { - if (ciDebugBar.readCookie('debug-view')) - { + if (ciDebugBar.readCookie("debug-view")) { hideHints(); - } - else - { + } else { showHints(); } }; // Determine Hints state on page load - if (ciDebugBar.readCookie('debug-view')) - { + if (ciDebugBar.readCookie("debug-view")) { showHints(); } }, setToolbarPosition: function () { - var btnPosition = this.toolbar.querySelector('#toolbar-position'); + var btnPosition = this.toolbar.querySelector("#toolbar-position"); - if (ciDebugBar.readCookie('debug-bar-position') === 'top') - { - ciDebugBar.addClass(ciDebugBar.icon, 'fixed-top'); - ciDebugBar.addClass(ciDebugBar.toolbar, 'fixed-top'); + if (ciDebugBar.readCookie("debug-bar-position") === "top") { + ciDebugBar.addClass(ciDebugBar.icon, "fixed-top"); + ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top"); } - btnPosition.addEventListener('click', function () { - var position = ciDebugBar.readCookie('debug-bar-position'); - - ciDebugBar.createCookie('debug-bar-position', '', -1); - - if (!position || position === 'bottom') - { - ciDebugBar.createCookie('debug-bar-position', 'top', 365); - ciDebugBar.addClass(ciDebugBar.icon, 'fixed-top'); - ciDebugBar.addClass(ciDebugBar.toolbar, 'fixed-top'); - } - else - { - ciDebugBar.createCookie('debug-bar-position', 'bottom', 365); - ciDebugBar.removeClass(ciDebugBar.icon, 'fixed-top'); - ciDebugBar.removeClass(ciDebugBar.toolbar, 'fixed-top'); - } - }, true); + btnPosition.addEventListener( + "click", + function () { + var position = ciDebugBar.readCookie("debug-bar-position"); + + ciDebugBar.createCookie("debug-bar-position", "", -1); + + if (!position || position === "bottom") { + ciDebugBar.createCookie("debug-bar-position", "top", 365); + ciDebugBar.addClass(ciDebugBar.icon, "fixed-top"); + ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top"); + } else { + ciDebugBar.createCookie( + "debug-bar-position", + "bottom", + 365 + ); + ciDebugBar.removeClass(ciDebugBar.icon, "fixed-top"); + ciDebugBar.removeClass(ciDebugBar.toolbar, "fixed-top"); + } + }, + true + ); }, setToolbarTheme: function () { - var btnTheme = this.toolbar.querySelector('#toolbar-theme'); - var isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches; - var isLightMode = window.matchMedia("(prefers-color-scheme: light)").matches; + var btnTheme = this.toolbar.querySelector("#toolbar-theme"); + var isDarkMode = window.matchMedia( + "(prefers-color-scheme: dark)" + ).matches; + var isLightMode = window.matchMedia( + "(prefers-color-scheme: light)" + ).matches; // If a cookie is set with a value, we force the color scheme - if (ciDebugBar.readCookie('debug-bar-theme') === 'dark') - { - ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'light'); - ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'dark'); + if (ciDebugBar.readCookie("debug-bar-theme") === "dark") { + ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "light"); + ciDebugBar.addClass(ciDebugBar.toolbarContainer, "dark"); + } else if (ciDebugBar.readCookie("debug-bar-theme") === "light") { + ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark"); + ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light"); } - else if (ciDebugBar.readCookie('debug-bar-theme') === 'light') - { - ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'dark'); - ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'light'); + + btnTheme.addEventListener( + "click", + function () { + var theme = ciDebugBar.readCookie("debug-bar-theme"); + + if ( + !theme && + window.matchMedia("(prefers-color-scheme: dark)").matches + ) { + // If there is no cookie, and "prefers-color-scheme" is set to "dark" + // It means that the user wants to switch to light mode + ciDebugBar.createCookie("debug-bar-theme", "light", 365); + ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark"); + ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light"); + } else { + if (theme === "dark") { + ciDebugBar.createCookie( + "debug-bar-theme", + "light", + 365 + ); + ciDebugBar.removeClass( + ciDebugBar.toolbarContainer, + "dark" + ); + ciDebugBar.addClass( + ciDebugBar.toolbarContainer, + "light" + ); + } else { + // In any other cases: if there is no cookie, or the cookie is set to + // "light", or the "prefers-color-scheme" is "light"... + ciDebugBar.createCookie("debug-bar-theme", "dark", 365); + ciDebugBar.removeClass( + ciDebugBar.toolbarContainer, + "light" + ); + ciDebugBar.addClass( + ciDebugBar.toolbarContainer, + "dark" + ); + } + } + }, + true + ); + }, + + setHotReloadState: function () { + var btn = document.getElementById("debug-hot-reload").parentNode; + var btnImg = btn.firstElementChild; + var eventSource; + + // If the Hot Reload Collector is inactive stops here + if (!btn) { + return; } - btnTheme.addEventListener('click', function () { - var theme = ciDebugBar.readCookie('debug-bar-theme'); + btn.onclick = function () { + if (ciDebugBar.readCookie("debug-hot-reload")) { + ciDebugBar.createCookie("debug-hot-reload", "", -1); + ciDebugBar.removeClass(btn, "active"); + ciDebugBar.removeClass(btnImg, "rotate"); - if (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches) - { - // If there is no cookie, and "prefers-color-scheme" is set to "dark" - // It means that the user wants to switch to light mode - ciDebugBar.createCookie('debug-bar-theme', 'light', 365); - ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'dark'); - ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'light'); - } - else - { - if (theme === 'dark') - { - ciDebugBar.createCookie('debug-bar-theme', 'light', 365); - ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'dark'); - ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'light'); - } - else - { - // In any other cases: if there is no cookie, or the cookie is set to - // "light", or the "prefers-color-scheme" is "light"... - ciDebugBar.createCookie('debug-bar-theme', 'dark', 365); - ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'light'); - ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'dark'); + // Close the EventSource connection if it exists + if (typeof eventSource !== "undefined") { + eventSource.close(); + eventSource = void 0; // Undefine the variable } + } else { + ciDebugBar.createCookie("debug-hot-reload", "show", 365); + ciDebugBar.addClass(btn, "active"); + ciDebugBar.addClass(btnImg, "rotate"); + + eventSource = ciDebugBar.hotReloadConnect(); } - }, true); + }; + + // Determine Hot Reload state on page load + if (ciDebugBar.readCookie("debug-hot-reload")) { + ciDebugBar.addClass(btn, "active"); + ciDebugBar.addClass(btnImg, "rotate"); + eventSource = ciDebugBar.hotReloadConnect(); + } + }, + + hotReloadConnect: function () { + const eventSource = new EventSource("/__hot-reload"); + + eventSource.addEventListener("reload", function (e) { + console.log("reload", e); + window.location.reload(); + }); + + eventSource.onerror = (err) => { + console.error("EventSource failed:", err); + }; + + return eventSource; }, /** @@ -585,103 +671,112 @@ var ciDebugBar = { * @param value * @param days */ - createCookie : function (name,value,days) { - if (days) - { + createCookie: function (name, value, days) { + if (days) { var date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); var expires = "; expires=" + date.toGMTString(); - } - else - { + } else { var expires = ""; } - document.cookie = name + "=" + value + expires + "; path=/; samesite=Lax"; + document.cookie = + name + "=" + value + expires + "; path=/; samesite=Lax"; }, - readCookie : function (name) { + readCookie: function (name) { var nameEQ = name + "="; - var ca = document.cookie.split(';'); + var ca = document.cookie.split(";"); - for (var i = 0; i < ca.length; i++) - { + for (var i = 0; i < ca.length; i++) { var c = ca[i]; - while (c.charAt(0) == ' ') - { - c = c.substring(1,c.length); + while (c.charAt(0) == " ") { + c = c.substring(1, c.length); } - if (c.indexOf(nameEQ) == 0) - { - return c.substring(nameEQ.length,c.length); + if (c.indexOf(nameEQ) == 0) { + return c.substring(nameEQ.length, c.length); } } return null; }, trimSlash: function (text) { - return text.replace(/^\/|\/$/g, ''); + return text.replace(/^\/|\/$/g, ""); }, routerLink: function () { var row, _location; - var rowGet = this.toolbar.querySelectorAll('td[data-debugbar-route="GET"]'); - var patt = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/; + var rowGet = this.toolbar.querySelectorAll( + 'td[data-debugbar-route="GET"]' + ); + var patt = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/; - for (var i = 0; i < rowGet.length; i++) - { + for (var i = 0; i < rowGet.length; i++) { row = rowGet[i]; - if (!/\/\(.+?\)/.test(rowGet[i].innerText)) - { - row.style = 'cursor: pointer;'; - row.setAttribute('title', location.origin + '/' + ciDebugBar.trimSlash(row.innerText)); - row.addEventListener('click', function (ev) { - _location = location.origin + '/' + ciDebugBar.trimSlash(ev.target.innerText); - var redirectWindow = window.open(_location, '_blank'); + if (!/\/\(.+?\)/.test(rowGet[i].innerText)) { + row.style = "cursor: pointer;"; + row.setAttribute( + "title", + location.origin + "/" + ciDebugBar.trimSlash(row.innerText) + ); + row.addEventListener("click", function (ev) { + _location = + location.origin + + "/" + + ciDebugBar.trimSlash(ev.target.innerText); + var redirectWindow = window.open(_location, "_blank"); redirectWindow.location; }); - } - else - { - row.innerHTML = '
' + row.innerText + '
' - + '
' - + row.innerText.replace(patt, '') - + '' - + '
'; + } else { + row.innerHTML = + "
" + + row.innerText + + "
" + + '
' + + row.innerText.replace( + patt, + '' + ) + + '' + + "
"; } } - rowGet = this.toolbar.querySelectorAll('td[data-debugbar-route="GET"] form'); - for (var i = 0; i < rowGet.length; i++) - { + rowGet = this.toolbar.querySelectorAll( + 'td[data-debugbar-route="GET"] form' + ); + for (var i = 0; i < rowGet.length; i++) { row = rowGet[i]; - row.addEventListener('submit', function (event) { - event.preventDefault() - var inputArray = [], t = 0; - var input = event.target.querySelectorAll('input[type=text]'); - var tpl = event.target.getAttribute('data-debugbar-route-tpl'); + row.addEventListener("submit", function (event) { + event.preventDefault(); + var inputArray = [], + t = 0; + var input = event.target.querySelectorAll("input[type=text]"); + var tpl = event.target.getAttribute("data-debugbar-route-tpl"); - for (var n = 0; n < input.length; n++) - { - if (input[n].value.length > 0) - { + for (var n = 0; n < input.length; n++) { + if (input[n].value.length > 0) { inputArray.push(input[n].value); } } - if (inputArray.length > 0) - { - _location = location.origin + '/' + tpl.replace(/\?/g, function () { - return inputArray[t++] - }); + if (inputArray.length > 0) { + _location = + location.origin + + "/" + + tpl.replace(/\?/g, function () { + return inputArray[t++]; + }); - var redirectWindow = window.open(_location, '_blank'); + var redirectWindow = window.open(_location, "_blank"); redirectWindow.location; } - }) + }); } - } + }, }; diff --git a/system/Debug/Toolbar/Views/toolbar.tpl.php b/system/Debug/Toolbar/Views/toolbar.tpl.php index 7c0d5336a5d4..30526726ca00 100644 --- a/system/Debug/Toolbar/Views/toolbar.tpl.php +++ b/system/Debug/Toolbar/Views/toolbar.tpl.php @@ -34,6 +34,11 @@
🔅 + + + + + diff --git a/system/HotReloader/DirectoryHasher.php b/system/HotReloader/DirectoryHasher.php new file mode 100644 index 000000000000..80df77aacec0 --- /dev/null +++ b/system/HotReloader/DirectoryHasher.php @@ -0,0 +1,61 @@ +hashApp())); + } + + /** + * Generates an array of md5 hashes for all directories that are + * watched by the Hot Reloader, as defined in the Config\Toolbar. + */ + public function hashApp(): array + { + $hashes = []; + + $watchedDirectories = config('Toolbar')->watchedDirectories; + + foreach ($watchedDirectories as $directory) { + if (is_dir(ROOTPATH . $directory)) { + $hashes[] = $this->hashDirectory(ROOTPATH . $directory); + } + } + + return array_unique(array_filter($hashes)); + } + + /** + * Generates an md5 hash of a given directory and all of its files + * that match the watched extensions defined in Config\Toolbar. + */ + public function hashDirectory(string $path): string + { + $directory = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS); + $filter = new IteratorFilter($directory); + $iterator = new RecursiveIteratorIterator($filter); + + $hashes = []; + + foreach ($iterator as $file) { + if ($file->isFile()) { + $hashes[] = md5_file($file->getRealPath()); + } + } + + return md5(implode('', $hashes)); + } +} diff --git a/system/HotReloader/HotReloader.php b/system/HotReloader/HotReloader.php new file mode 100644 index 000000000000..14efd297a39b --- /dev/null +++ b/system/HotReloader/HotReloader.php @@ -0,0 +1,52 @@ +hash(); + + while (true) { + if( connection_status() != CONNECTION_NORMAL or connection_aborted() ) { + break; + } + + $currentHash = $hasher->hash(); + + // If hash has changed, tell the browser to reload. + if ($currentHash !== $appHash) { + $appHash = $currentHash; + + $this->sendEvent('reload', ['time' => date('Y-m-d H:i:s')]); + break; + } + + sleep(1); + } + } + + /** + * Send an event to the browser. + */ + private function sendEvent(string $event, array $data): void + { + echo "event: {$event}\n"; + echo "data: " . json_encode($data) ."\n\n"; + + ob_flush(); + flush(); + } +} diff --git a/system/HotReloader/IteratorFilter.php b/system/HotReloader/IteratorFilter.php new file mode 100644 index 000000000000..903f7256f2af --- /dev/null +++ b/system/HotReloader/IteratorFilter.php @@ -0,0 +1,39 @@ +watchedExtensions = config('Toolbar')->watchedExtensions; + } + + /** + * Apply filters to the files in the iterator. + */ + public function accept(): bool + { + if (! $this->current()->isFile()) { + return true; + } + + $filename = $this->current()->getFilename(); + + // Skip hidden files and directories. + if ($filename[0] === '.') { + return false; + } + + // Only consume files of interest. + $ext = trim(strtolower($this->current()->getExtension()), '. '); + return in_array($ext, $this->watchedExtensions); + } +} diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index a90187940cf1..432e5e12ae20 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -56,6 +56,8 @@ Method Signature Changes Enhancements ************ +- The Debug Toolbar now has a new "Hot Reload" feature that can be used to automatically reload the page when a file is changed. See :ref:`debug-toolbar-hot-reload`. + Commands ======== diff --git a/user_guide_src/source/testing/debugging.rst b/user_guide_src/source/testing/debugging.rst index b8da022d018f..84ebe8ce5950 100644 --- a/user_guide_src/source/testing/debugging.rst +++ b/user_guide_src/source/testing/debugging.rst @@ -172,3 +172,14 @@ The ``getVarData()`` method should return an array containing arrays of key/valu outer array's key is the name of the section on the Vars tab: .. literalinclude:: debugging/006.php + +Hot Reloading +============= + +The Debug Toolbar includes a feature called Hot Reloading that allows you to make changes to your application's code and have them automatically reloaded in the browser without having to refresh the page. This is a great time-saver during development. + +To enable Hot Reloading while you are developing, you can click the button on the left side of the toolbar that looks like a refresh icon. This will enable Hot Reloading for all pages until you disable it. + +Hot Reloading works by scanning the files within the ``app`` directory every second and looking for changes. If it finds any, it will send a message to the browser to reload the page. It does not scan any other directories, so if you are making changes to files outside of the ``app`` directory, you will need to manually refresh the page. + +If you need to watch files outside of the ``app`` directory, or are finding it slow due to the size of your project, you can specify the directories to scan and the file extensions to scan for in the ``$toolbarRefreshDirs`` and ``$toolbarRefreshExtensions`` properties of the **app/Config/Toolbar.php** configuration file. From 3840990d1df9dfdb2b2732ea04b038fa254d9eb1 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 14 May 2023 23:32:48 -0500 Subject: [PATCH 243/485] Keep nginx from sending 504 time-out errors Co-authored-by: Michal Sniatala --- system/HotReloader/HotReloader.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/HotReloader/HotReloader.php b/system/HotReloader/HotReloader.php index 14efd297a39b..ab40dfef734e 100644 --- a/system/HotReloader/HotReloader.php +++ b/system/HotReloader/HotReloader.php @@ -32,6 +32,8 @@ public function run() $this->sendEvent('reload', ['time' => date('Y-m-d H:i:s')]); break; + } elseif (rand(1, 10) > 8) { + $this->sendEvent('ping', ['time' => date('Y-m-d H:i:s')]); } sleep(1); From a22a8c1697cec27b458673f28d3eeee6792086ab Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 14 May 2023 23:51:55 -0500 Subject: [PATCH 244/485] fix: Keep scrollbar from showing up due to rotating icon --- system/Debug/Toolbar/Views/toolbar.tpl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Debug/Toolbar/Views/toolbar.tpl.php b/system/Debug/Toolbar/Views/toolbar.tpl.php index 30526726ca00..8a978d63800c 100644 --- a/system/Debug/Toolbar/Views/toolbar.tpl.php +++ b/system/Debug/Toolbar/Views/toolbar.tpl.php @@ -34,7 +34,7 @@
🔅 - + From 8be1d5f3f62775df41860da643a2317450df1169 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 14 May 2023 23:52:09 -0500 Subject: [PATCH 245/485] fix: Remove output_buffering call that cannot actually be set with ini_set --- system/HotReloader/HotReloader.php | 1 - 1 file changed, 1 deletion(-) diff --git a/system/HotReloader/HotReloader.php b/system/HotReloader/HotReloader.php index ab40dfef734e..fd02e3f16199 100644 --- a/system/HotReloader/HotReloader.php +++ b/system/HotReloader/HotReloader.php @@ -6,7 +6,6 @@ class HotReloader { public function run() { - ini_set('output_buffering', 'Off'); ini_set('zlib.output_compression', 'Off'); header("Cache-Control: no-store"); From caeaf5df7f3e2213df40f9b596d705471eef6ec3 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 14 May 2023 23:56:01 -0500 Subject: [PATCH 246/485] fix: Make the hot reload script work for subfolder hosting. --- system/Debug/Toolbar/Views/toolbar.js | 2 +- system/Debug/Toolbar/Views/toolbar.tpl.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/system/Debug/Toolbar/Views/toolbar.js b/system/Debug/Toolbar/Views/toolbar.js index bfff574507e7..928dc947f768 100644 --- a/system/Debug/Toolbar/Views/toolbar.js +++ b/system/Debug/Toolbar/Views/toolbar.js @@ -650,7 +650,7 @@ var ciDebugBar = { }, hotReloadConnect: function () { - const eventSource = new EventSource("/__hot-reload"); + const eventSource = new EventSource(ciSiteURL + "/__hot-reload"); eventSource.addEventListener("reload", function (e) { console.log("reload", e); diff --git a/system/Debug/Toolbar/Views/toolbar.tpl.php b/system/Debug/Toolbar/Views/toolbar.tpl.php index 8a978d63800c..3bc3df133b69 100644 --- a/system/Debug/Toolbar/Views/toolbar.tpl.php +++ b/system/Debug/Toolbar/Views/toolbar.tpl.php @@ -23,6 +23,7 @@
From aa800a99d5e74e8f6a7d718b0b7026a6d10a977a Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 22 May 2023 22:25:55 -0500 Subject: [PATCH 247/485] Add tests for Directory Hasher. --- system/Exceptions/FrameworkException.php | 5 ++ system/HotReloader/DirectoryHasher.php | 7 ++- system/Language/en/Core.php | 1 + .../HotReloader/DirectoryHasherTest.php | 58 +++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 tests/system/HotReloader/DirectoryHasherTest.php diff --git a/system/Exceptions/FrameworkException.php b/system/Exceptions/FrameworkException.php index 333f999df84a..cee5f903e785 100644 --- a/system/Exceptions/FrameworkException.php +++ b/system/Exceptions/FrameworkException.php @@ -33,6 +33,11 @@ public static function forInvalidFile(string $path) return new static(lang('Core.invalidFile', [$path])); } + public static function forInvalidDirectory(string $path) + { + return new static(lang('Core.invalidDirectory', [$path])); + } + public static function forCopyError(string $path) { return new static(lang('Core.copyError', [$path])); diff --git a/system/HotReloader/DirectoryHasher.php b/system/HotReloader/DirectoryHasher.php index 80df77aacec0..7658ece8fdf3 100644 --- a/system/HotReloader/DirectoryHasher.php +++ b/system/HotReloader/DirectoryHasher.php @@ -2,6 +2,7 @@ namespace CodeIgniter\HotReloader; +use CodeIgniter\Exceptions\FrameworkException; use FilesystemIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -31,7 +32,7 @@ public function hashApp(): array foreach ($watchedDirectories as $directory) { if (is_dir(ROOTPATH . $directory)) { - $hashes[] = $this->hashDirectory(ROOTPATH . $directory); + $hashes[$directory] = $this->hashDirectory(ROOTPATH . $directory); } } @@ -44,6 +45,10 @@ public function hashApp(): array */ public function hashDirectory(string $path): string { + if (! is_dir($path)) { + throw FrameworkException::forInvalidDirectory($path); + } + $directory = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS); $filter = new IteratorFilter($directory); $iterator = new RecursiveIteratorIterator($filter); diff --git a/system/Language/en/Core.php b/system/Language/en/Core.php index 9ab549a4c8fe..1adfbe446560 100644 --- a/system/Language/en/Core.php +++ b/system/Language/en/Core.php @@ -14,6 +14,7 @@ 'copyError' => 'An error was encountered while attempting to replace the file "{0}". Please make sure your file directory is writable.', 'enabledZlibOutputCompression' => 'Your zlib.output_compression ini directive is turned on. This will not work well with output buffers.', 'invalidFile' => 'Invalid file: "{0}"', + 'invalidDirectory' => 'Directory does not exist: "{0}"', 'invalidPhpVersion' => 'Your PHP version must be {0} or higher to run CodeIgniter. Current version: {1}', 'missingExtension' => 'The framework needs the following extension(s) installed and loaded: "{0}".', 'noHandlers' => '"{0}" must provide at least one Handler.', diff --git a/tests/system/HotReloader/DirectoryHasherTest.php b/tests/system/HotReloader/DirectoryHasherTest.php new file mode 100644 index 000000000000..48c19cf4baba --- /dev/null +++ b/tests/system/HotReloader/DirectoryHasherTest.php @@ -0,0 +1,58 @@ +hasher = new DirectoryHasher(); + } + + public function testHashApp() + { + $results = $this->hasher->hashApp(); + + $this->assertIsArray($results); + $this->assertArrayHasKey('app', $results); + } + + public function testHashDirectoryInvalid() + { + $this->expectException(FrameworkException::class); + $this->expectExceptionMessage('Directory does not exist: "' . APPPATH . 'Foo"'); + + $this->hasher->hashDirectory(APPPATH . 'Foo'); + } + + public function testUniqueHashes() + { + $hash1 = $this->hasher->hashDirectory(APPPATH); + $hash2 = $this->hasher->hashDirectory(SYSTEMPATH); + + $this->assertNotEquals($hash1, $hash2); + } + + public function testRepeatableHashes() + { + $hash1 = $this->hasher->hashDirectory(APPPATH); + $hash2 = $this->hasher->hashDirectory(APPPATH); + + $this->assertEquals($hash1, $hash2); + } + + public function testHash() + { + $expected = md5(implode('', $this->hasher->hashApp())); + + $this->assertEquals($expected, $this->hasher->hash()); + } +} From 470bcb9234499a47b45c4a63b64164b06a39f9e7 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 22 May 2023 22:30:25 -0500 Subject: [PATCH 248/485] Added michalsn's suggestion to rotate the image not the anchor. --- system/Debug/Toolbar/Views/toolbar.js | 2 +- system/Debug/Toolbar/Views/toolbar.tpl.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Debug/Toolbar/Views/toolbar.js b/system/Debug/Toolbar/Views/toolbar.js index 928dc947f768..073a76ab4312 100644 --- a/system/Debug/Toolbar/Views/toolbar.js +++ b/system/Debug/Toolbar/Views/toolbar.js @@ -613,7 +613,7 @@ var ciDebugBar = { setHotReloadState: function () { var btn = document.getElementById("debug-hot-reload").parentNode; - var btnImg = btn.firstElementChild; + var btnImg = btn.getElementsByTagName("img")[0]; var eventSource; // If the Hot Reload Collector is inactive stops here diff --git a/system/Debug/Toolbar/Views/toolbar.tpl.php b/system/Debug/Toolbar/Views/toolbar.tpl.php index 3bc3df133b69..6e62340c6ded 100644 --- a/system/Debug/Toolbar/Views/toolbar.tpl.php +++ b/system/Debug/Toolbar/Views/toolbar.tpl.php @@ -35,7 +35,7 @@
🔅 - + From d2c8e455ee701d13f996b431850a1affbd28636e Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 22 May 2023 22:43:48 -0500 Subject: [PATCH 249/485] fix: attempting to fix style, user guide, and other small errors --- user_guide_src/source/testing/debugging.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/testing/debugging.rst b/user_guide_src/source/testing/debugging.rst index 84ebe8ce5950..138bf49af2bf 100644 --- a/user_guide_src/source/testing/debugging.rst +++ b/user_guide_src/source/testing/debugging.rst @@ -173,6 +173,8 @@ outer array's key is the name of the section on the Vars tab: .. literalinclude:: debugging/006.php +.. _debug-toolbar-hot-reload: + Hot Reloading ============= From c6b6341207ac038bd98bb20473bee0027d3864d7 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 22 May 2023 22:44:12 -0500 Subject: [PATCH 250/485] fix: attempting to fix style, user guide, and other small errors --- app/Config/Events.php | 5 +++-- app/Config/Toolbar.php | 2 +- system/HotReloader/DirectoryHasher.php | 3 +++ system/HotReloader/HotReloader.php | 3 +++ system/HotReloader/IteratorFilter.php | 5 ++++- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/Config/Events.php b/app/Config/Events.php index bb48c66efde8..e3cebdd3d2bd 100644 --- a/app/Config/Events.php +++ b/app/Config/Events.php @@ -4,6 +4,7 @@ use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\FrameworkException; +use CodeIgniter\HotReloader\HotReloader; /* * -------------------------------------------------------------------- @@ -46,8 +47,8 @@ Services::toolbar()->respond(); // Hot Reload route - for framework use on the hot reloader. if (ENVIRONMENT === 'development') { - Services::routes()->get('__hot-reload', function() { - (new \CodeIgniter\HotReloader\HotReloader())->run(); + Services::routes()->get('__hot-reload', static function() { + (new HotReloader())->run(); }); } } diff --git a/app/Config/Toolbar.php b/app/Config/Toolbar.php index 38a42b59f1cf..97fbda281287 100644 --- a/app/Config/Toolbar.php +++ b/app/Config/Toolbar.php @@ -113,6 +113,6 @@ class Toolbar extends BaseConfig * used to determine if the hot-reload feature should reload the page or not. */ public array $watchedExtensions = [ - 'php', 'css', 'js', 'html', 'svg', 'json', 'env' + 'php', 'css', 'js', 'html', 'svg', 'json', 'env', ]; } diff --git a/system/HotReloader/DirectoryHasher.php b/system/HotReloader/DirectoryHasher.php index 7658ece8fdf3..b6acab63c311 100644 --- a/system/HotReloader/DirectoryHasher.php +++ b/system/HotReloader/DirectoryHasher.php @@ -7,6 +7,9 @@ use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +/** + * @internal + */ class DirectoryHasher { /** diff --git a/system/HotReloader/HotReloader.php b/system/HotReloader/HotReloader.php index fd02e3f16199..3834042e4ce0 100644 --- a/system/HotReloader/HotReloader.php +++ b/system/HotReloader/HotReloader.php @@ -2,6 +2,9 @@ namespace CodeIgniter\HotReloader; +/** + * @internal + */ class HotReloader { public function run() diff --git a/system/HotReloader/IteratorFilter.php b/system/HotReloader/IteratorFilter.php index 903f7256f2af..bef1d20e4c31 100644 --- a/system/HotReloader/IteratorFilter.php +++ b/system/HotReloader/IteratorFilter.php @@ -5,7 +5,10 @@ use RecursiveFilterIterator; use RecursiveIterator; -class IteratorFilter extends RecursiveFilterIterator +/** + * @internal + */ +class IteratorFilter extends RecursiveFilterIterator implements RecursiveIterator { private array $watchedExtensions = []; From 2b52354fa98fd7a44faad8706b061741a785e4cd Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 22 May 2023 22:46:46 -0500 Subject: [PATCH 251/485] fix: additional code style fix --- app/Config/Events.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Config/Events.php b/app/Config/Events.php index e3cebdd3d2bd..993abd24ebc7 100644 --- a/app/Config/Events.php +++ b/app/Config/Events.php @@ -47,7 +47,7 @@ Services::toolbar()->respond(); // Hot Reload route - for framework use on the hot reloader. if (ENVIRONMENT === 'development') { - Services::routes()->get('__hot-reload', static function() { + Services::routes()->get('__hot-reload', static function () { (new HotReloader())->run(); }); } From 4463200e0812c23eb9d75c715ddf7bd5c624fdf7 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 22 May 2023 23:15:57 -0500 Subject: [PATCH 252/485] More style fixes --- system/HotReloader/DirectoryHasher.php | 15 +++++++++--- system/HotReloader/HotReloader.php | 24 +++++++++++++------ system/HotReloader/IteratorFilter.php | 14 +++++++++-- .../HotReloader/DirectoryHasherTest.php | 22 +++++++++++++---- 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/system/HotReloader/DirectoryHasher.php b/system/HotReloader/DirectoryHasher.php index b6acab63c311..fac072fdb24a 100644 --- a/system/HotReloader/DirectoryHasher.php +++ b/system/HotReloader/DirectoryHasher.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace CodeIgniter\HotReloader; use CodeIgniter\Exceptions\FrameworkException; @@ -10,7 +19,7 @@ /** * @internal */ -class DirectoryHasher +final class DirectoryHasher { /** * Generates an md5 value of all directories that are watched by the @@ -53,8 +62,8 @@ public function hashDirectory(string $path): string } $directory = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS); - $filter = new IteratorFilter($directory); - $iterator = new RecursiveIteratorIterator($filter); + $filter = new IteratorFilter($directory); + $iterator = new RecursiveIteratorIterator($filter); $hashes = []; diff --git a/system/HotReloader/HotReloader.php b/system/HotReloader/HotReloader.php index 3834042e4ce0..2f10fae6a566 100644 --- a/system/HotReloader/HotReloader.php +++ b/system/HotReloader/HotReloader.php @@ -1,28 +1,37 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace CodeIgniter\HotReloader; /** * @internal */ -class HotReloader +final class HotReloader { public function run() { ini_set('zlib.output_compression', 'Off'); - header("Cache-Control: no-store"); - header("Content-Type: text/event-stream"); + header('Cache-Control: no-store'); + header('Content-Type: text/event-stream'); header('Access-Control-Allow-Methods: GET'); ob_end_clean(); set_time_limit(0); - $hasher = new DirectoryHasher(); + $hasher = new DirectoryHasher(); $appHash = $hasher->hash(); while (true) { - if( connection_status() != CONNECTION_NORMAL or connection_aborted() ) { + if (connection_status() !== CONNECTION_NORMAL || connection_aborted()) { break; } @@ -34,7 +43,8 @@ public function run() $this->sendEvent('reload', ['time' => date('Y-m-d H:i:s')]); break; - } elseif (rand(1, 10) > 8) { + } + if (mt_rand(1, 10) > 8) { $this->sendEvent('ping', ['time' => date('Y-m-d H:i:s')]); } @@ -48,7 +58,7 @@ public function run() private function sendEvent(string $event, array $data): void { echo "event: {$event}\n"; - echo "data: " . json_encode($data) ."\n\n"; + echo 'data: ' . json_encode($data) . "\n\n"; ob_flush(); flush(); diff --git a/system/HotReloader/IteratorFilter.php b/system/HotReloader/IteratorFilter.php index bef1d20e4c31..f3ce7aecf9ef 100644 --- a/system/HotReloader/IteratorFilter.php +++ b/system/HotReloader/IteratorFilter.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace CodeIgniter\HotReloader; use RecursiveFilterIterator; @@ -8,7 +17,7 @@ /** * @internal */ -class IteratorFilter extends RecursiveFilterIterator implements RecursiveIterator +final class IteratorFilter extends RecursiveFilterIterator implements RecursiveIterator { private array $watchedExtensions = []; @@ -37,6 +46,7 @@ public function accept(): bool // Only consume files of interest. $ext = trim(strtolower($this->current()->getExtension()), '. '); - return in_array($ext, $this->watchedExtensions); + + return in_array($ext, $this->watchedExtensions, true); } } diff --git a/tests/system/HotReloader/DirectoryHasherTest.php b/tests/system/HotReloader/DirectoryHasherTest.php index 48c19cf4baba..a6113443ce33 100644 --- a/tests/system/HotReloader/DirectoryHasherTest.php +++ b/tests/system/HotReloader/DirectoryHasherTest.php @@ -1,16 +1,28 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace Tests\System\HotReloader; use CodeIgniter\Exceptions\FrameworkException; use CodeIgniter\HotReloader\DirectoryHasher; use CodeIgniter\Test\CIUnitTestCase; -class DirectoryHasherTest extends CIUnitTestCase +/** + * @internal + */ +final class DirectoryHasherTest extends CIUnitTestCase { protected DirectoryHasher $hasher; - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -38,7 +50,7 @@ public function testUniqueHashes() $hash1 = $this->hasher->hashDirectory(APPPATH); $hash2 = $this->hasher->hashDirectory(SYSTEMPATH); - $this->assertNotEquals($hash1, $hash2); + $this->assertNotSame($hash1, $hash2); } public function testRepeatableHashes() @@ -46,13 +58,13 @@ public function testRepeatableHashes() $hash1 = $this->hasher->hashDirectory(APPPATH); $hash2 = $this->hasher->hashDirectory(APPPATH); - $this->assertEquals($hash1, $hash2); + $this->assertSame($hash1, $hash2); } public function testHash() { $expected = md5(implode('', $this->hasher->hashApp())); - $this->assertEquals($expected, $this->hasher->hash()); + $this->assertSame($expected, $this->hasher->hash()); } } From 884a91e3dabd867c74c7fc87c9490ff0d64a9ce8 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 5 Jun 2023 22:29:13 -0500 Subject: [PATCH 253/485] Rector updates --- tests/system/HotReloader/DirectoryHasherTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/HotReloader/DirectoryHasherTest.php b/tests/system/HotReloader/DirectoryHasherTest.php index a6113443ce33..c10bae8fa7c0 100644 --- a/tests/system/HotReloader/DirectoryHasherTest.php +++ b/tests/system/HotReloader/DirectoryHasherTest.php @@ -9,7 +9,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace Tests\System\HotReloader; +namespace CodeIgniter\HotReloader; use CodeIgniter\Exceptions\FrameworkException; use CodeIgniter\HotReloader\DirectoryHasher; @@ -20,7 +20,7 @@ */ final class DirectoryHasherTest extends CIUnitTestCase { - protected DirectoryHasher $hasher; + private DirectoryHasher $hasher; protected function setUp(): void { From 0bca6467f7149be6cc876dc9f183933fe676ea06 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 23 Jun 2023 23:34:27 -0500 Subject: [PATCH 254/485] Run cs-fix --- tests/system/HotReloader/DirectoryHasherTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/system/HotReloader/DirectoryHasherTest.php b/tests/system/HotReloader/DirectoryHasherTest.php index c10bae8fa7c0..08f2ea6eef2a 100644 --- a/tests/system/HotReloader/DirectoryHasherTest.php +++ b/tests/system/HotReloader/DirectoryHasherTest.php @@ -12,7 +12,6 @@ namespace CodeIgniter\HotReloader; use CodeIgniter\Exceptions\FrameworkException; -use CodeIgniter\HotReloader\DirectoryHasher; use CodeIgniter\Test\CIUnitTestCase; /** From 3fd7e4ba8d9fd119e327e4d098de197adf23a1f2 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 23 Jun 2023 23:47:04 -0500 Subject: [PATCH 255/485] CSS corrections, and other utility fixes --- admin/css/debug-toolbar/toolbar.scss | 12 +++++++++++- system/Debug/Toolbar/Views/toolbar.css | 1 + system/HotReloader/IteratorFilter.php | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/admin/css/debug-toolbar/toolbar.scss b/admin/css/debug-toolbar/toolbar.scss index 23e0807fabfc..01dfd2207b15 100644 --- a/admin/css/debug-toolbar/toolbar.scss +++ b/admin/css/debug-toolbar/toolbar.scss @@ -198,7 +198,7 @@ overflow: hidden; overflow-y: auto; padding: 0 12px 0 12px; - + // Give room for OS X scrollbar white-space: nowrap; z-index: 10000; @@ -501,3 +501,13 @@ .debug-bar-noverflow { overflow: hidden; } + +/* ENDLESS ROTATE */ +.rotate { + animation: rotate 9s linear infinite; +} +@keyframes rotate { + to { + transform: rotate(360deg); + } +} diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index b8954dacfde3..60c3216bfc9c 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -807,6 +807,7 @@ .rotate { animation: rotate 9s linear infinite; } + @keyframes rotate { to { transform: rotate(360deg); diff --git a/system/HotReloader/IteratorFilter.php b/system/HotReloader/IteratorFilter.php index f3ce7aecf9ef..b98a028d2443 100644 --- a/system/HotReloader/IteratorFilter.php +++ b/system/HotReloader/IteratorFilter.php @@ -16,6 +16,9 @@ /** * @internal + * + * @template-extends RecursiveFilterIterator + * @template-implements RecursiveIterator */ final class IteratorFilter extends RecursiveFilterIterator implements RecursiveIterator { From a1bbe2dbce26e0c0b4a20bf4c8604147884563ef Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 23 Jun 2023 23:53:39 -0500 Subject: [PATCH 256/485] Attempting to make psalm happy --- system/HotReloader/IteratorFilter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/HotReloader/IteratorFilter.php b/system/HotReloader/IteratorFilter.php index b98a028d2443..753cfdf8b1a9 100644 --- a/system/HotReloader/IteratorFilter.php +++ b/system/HotReloader/IteratorFilter.php @@ -17,8 +17,8 @@ /** * @internal * - * @template-extends RecursiveFilterIterator - * @template-implements RecursiveIterator + * @extends RecursiveFilterIterator + * @implements RecursiveIterator */ final class IteratorFilter extends RecursiveFilterIterator implements RecursiveIterator { From 7fd7cca6556f5cef3fa43edaa23c19c480afbb59 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sat, 24 Jun 2023 23:51:41 -0500 Subject: [PATCH 257/485] Run sass converter to generate toolbar.css --- system/Debug/Toolbar/Views/toolbar.css | 878 ++++++++++++------------- 1 file changed, 436 insertions(+), 442 deletions(-) diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index 60c3216bfc9c..38bab087ae81 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -7,327 +7,321 @@ * file that was distributed with this source code. */ #debug-icon { - bottom: 0; - position: fixed; - right: 0; - z-index: 10000; - height: 36px; - width: 36px; - margin: 0px; - padding: 0px; - clear: both; - text-align: center; + bottom: 0; + position: fixed; + right: 0; + z-index: 10000; + height: 36px; + width: 36px; + margin: 0px; + padding: 0px; + clear: both; + text-align: center; } #debug-icon a svg { - margin: 8px; - max-width: 20px; - max-height: 20px; + margin: 8px; + max-width: 20px; + max-height: 20px; } #debug-icon.fixed-top { - bottom: auto; - top: 0; + bottom: auto; + top: 0; } #debug-icon .debug-bar-ndisplay { - display: none; + display: none; } #debug-bar { - bottom: 0; - left: 0; - position: fixed; - right: 0; - z-index: 10000; - height: 36px; - line-height: 36px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, - sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; - font-size: 16px; - font-weight: 400; + bottom: 0; + left: 0; + position: fixed; + right: 0; + z-index: 10000; + height: 36px; + line-height: 36px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + font-size: 16px; + font-weight: 400; } #debug-bar h1 { - display: flex; - font-weight: normal; - margin: 0 0 0 auto; + display: flex; + font-weight: normal; + margin: 0 0 0 auto; } #debug-bar h1 svg { - width: 16px; - margin-right: 5px; + width: 16px; + margin-right: 5px; } #debug-bar h2 { - font-size: 16px; - margin: 0; - padding: 5px 0 10px 0; + font-size: 16px; + margin: 0; + padding: 5px 0 10px 0; } #debug-bar h2 span { - font-size: 13px; + font-size: 13px; } #debug-bar h3 { - font-size: 12px; - font-weight: 200; - margin: 0 0 0 10px; - padding: 0; - text-transform: uppercase; + font-size: 12px; + font-weight: 200; + margin: 0 0 0 10px; + padding: 0; + text-transform: uppercase; } #debug-bar p { - font-size: 12px; - margin: 0 0 0 15px; - padding: 0; + font-size: 12px; + margin: 0 0 0 15px; + padding: 0; } #debug-bar a { - text-decoration: none; + text-decoration: none; } #debug-bar a:hover { - text-decoration: underline; + text-decoration: underline; } #debug-bar button { - border: 1px solid; - border-radius: 4px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - cursor: pointer; - line-height: 15px; + border: 1px solid; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + cursor: pointer; + line-height: 15px; } #debug-bar button:hover { - text-decoration: underline; + text-decoration: underline; } #debug-bar table { - border-collapse: collapse; - font-size: 14px; - line-height: normal; - margin: 5px 10px 15px 10px; - width: calc(100% - 10px); + border-collapse: collapse; + font-size: 14px; + line-height: normal; + margin: 5px 10px 15px 10px; + width: calc(100% - 10px); } #debug-bar table strong { - font-weight: 500; + font-weight: 500; } #debug-bar table th { - display: table-cell; - font-weight: 600; - padding-bottom: 0.7em; - text-align: left; + display: table-cell; + font-weight: 600; + padding-bottom: 0.7em; + text-align: left; } #debug-bar table tr { - border: none; + border: none; } #debug-bar table td { - border: none; - display: table-cell; - margin: 0; - text-align: left; + border: none; + display: table-cell; + margin: 0; + text-align: left; } #debug-bar table td:first-child { - max-width: 20%; + max-width: 20%; } #debug-bar table td:first-child.narrow { - width: 7em; + width: 7em; } #debug-bar td[data-debugbar-route] form { - display: none; + display: none; } #debug-bar td[data-debugbar-route]:hover form { - display: block; + display: block; } #debug-bar td[data-debugbar-route]:hover > div { - display: none; + display: none; } -#debug-bar td[data-debugbar-route] input[type="text"] { - padding: 2px; +#debug-bar td[data-debugbar-route] input[type=text] { + padding: 2px; } #debug-bar .toolbar { - display: flex; - overflow: hidden; - overflow-y: auto; - padding: 0 12px 0 12px; - white-space: nowrap; - z-index: 10000; + display: flex; + overflow: hidden; + overflow-y: auto; + padding: 0 12px 0 12px; + white-space: nowrap; + z-index: 10000; } #debug-bar.fixed-top { - bottom: auto; - top: 0; + bottom: auto; + top: 0; } #debug-bar.fixed-top .tab { - bottom: auto; - top: 36px; + bottom: auto; + top: 36px; } #debug-bar #toolbar-position a, #debug-bar #toolbar-theme a { - padding: 0 6px; - display: inline-flex; - vertical-align: top; + padding: 0 6px; + display: inline-flex; + vertical-align: top; } #debug-bar #toolbar-position a:hover, #debug-bar #toolbar-theme a:hover { - text-decoration: none; + text-decoration: none; } #debug-bar #debug-bar-link { - display: flex; - padding: 6px; + display: flex; + padding: 6px; } #debug-bar .ci-label { - display: inline-flex; - font-size: 14px; + display: inline-flex; + font-size: 14px; } #debug-bar .ci-label:hover { - cursor: pointer; + cursor: pointer; } #debug-bar .ci-label a { - color: inherit; - display: flex; - letter-spacing: normal; - padding: 0 10px; - text-decoration: none; - align-items: center; + color: inherit; + display: flex; + letter-spacing: normal; + padding: 0 10px; + text-decoration: none; + align-items: center; } #debug-bar .ci-label img { - margin: 6px 3px 6px 0; - width: 16px !important; + margin: 6px 3px 6px 0; + width: 16px !important; } #debug-bar .ci-label .badge { - border-radius: 12px; - -moz-border-radius: 12px; - -webkit-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; + border-radius: 12px; + -moz-border-radius: 12px; + -webkit-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; } #debug-bar .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; + bottom: 35px; + display: none; + left: 0; + max-height: 62%; + overflow: hidden; + overflow-y: auto; + padding: 1em 2em; + position: fixed; + right: 0; + z-index: 9999; } #debug-bar .timeline { - margin-left: 0; - width: 100%; + margin-left: 0; + width: 100%; } #debug-bar .timeline th { - border-left: 1px solid; - font-size: 12px; - font-weight: 200; - padding: 5px 5px 10px 5px; - position: relative; - text-align: left; + border-left: 1px solid; + font-size: 12px; + font-weight: 200; + padding: 5px 5px 10px 5px; + position: relative; + text-align: left; } #debug-bar .timeline th:first-child { - border-left: 0; + border-left: 0; } #debug-bar .timeline td { - border-left: 1px solid; - padding: 5px; - position: relative; + border-left: 1px solid; + padding: 5px; + position: relative; } #debug-bar .timeline td:first-child { - border-left: 0; - max-width: none; + border-left: 0; + max-width: none; } #debug-bar .timeline td.child-container { - padding: 0px; + padding: 0px; } #debug-bar .timeline td.child-container .timeline { - margin: 0px; + 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 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; - -webkit-border-radius: 4px; - display: inline-block; - padding: 5px; - position: absolute; - top: 30%; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + display: inline-block; + padding: 5px; + position: absolute; + top: 30%; } #debug-bar .timeline .timeline-parent { - cursor: pointer; + 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; + 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; + background-color: #DFDFDF; } #debug-bar .timeline .timeline-parent-open td:first-child nav { - background-position: 0 75%; + background-position: 0 75%; } #debug-bar .timeline .child-row:hover { - background: transparent; + background: transparent; } #debug-bar .route-params, #debug-bar .route-params-item { - vertical-align: top; + vertical-align: top; } #debug-bar .route-params td:first-child, #debug-bar .route-params-item td:first-child { - font-style: italic; - padding-left: 1em; - text-align: right; + font-style: italic; + padding-left: 1em; + text-align: right; } .debug-view.show-view { - border: 1px solid; - margin: 4px; + border: 1px solid; + margin: 4px; } .debug-view-path { - font-family: monospace; - font-size: 12px; - letter-spacing: normal; - min-height: 16px; - padding: 2px; - text-align: left; + font-family: monospace; + font-size: 12px; + letter-spacing: normal; + min-height: 16px; + padding: 2px; + text-align: left; } .show-view .debug-view-path { - display: block !important; + display: block !important; } @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; + } } #debug-icon { - background-color: #ffffff; - box-shadow: 0 0 4px #dfdfdf; - -moz-box-shadow: 0 0 4px #dfdfdf; - -webkit-box-shadow: 0 0 4px #dfdfdf; + background-color: #FFFFFF; + 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 { - color: #dd8615; + color: #DD8615; } #debug-bar { - background-color: #ffffff; - color: #434343; + background-color: #FFFFFF; + color: #434343; } #debug-bar h1, #debug-bar h2, @@ -341,217 +335,217 @@ #debug-bar td, #debug-bar button, #debug-bar .toolbar { - background-color: transparent; - color: #434343; + background-color: transparent; + color: #434343; } #debug-bar button { - background-color: #ffffff; + background-color: #FFFFFF; } #debug-bar table strong { - color: #dd8615; + color: #DD8615; } #debug-bar table tbody tr:hover { - background-color: #dfdfdf; + background-color: #DFDFDF; } #debug-bar table tbody tr.current { - background-color: #fdc894; + background-color: #FDC894; } #debug-bar table tbody tr.current:hover td { - background-color: #dd4814; - color: #ffffff; + background-color: #DD4814; + color: #FFFFFF; } #debug-bar .toolbar { - background-color: #ffffff; - box-shadow: 0 0 4px #dfdfdf; - -moz-box-shadow: 0 0 4px #dfdfdf; - -webkit-box-shadow: 0 0 4px #dfdfdf; + background-color: #FFFFFF; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; } #debug-bar .toolbar img { - filter: brightness(0) invert(0.4); + filter: brightness(0) invert(0.4); } #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #dfdfdf; - -moz-box-shadow: 0 0 4px #dfdfdf; - -webkit-box-shadow: 0 0 4px #dfdfdf; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; } #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #dfdfdf; - -moz-box-shadow: 0 1px 4px #dfdfdf; - -webkit-box-shadow: 0 1px 4px #dfdfdf; + box-shadow: 0 1px 4px #DFDFDF; + -moz-box-shadow: 0 1px 4px #DFDFDF; + -webkit-box-shadow: 0 1px 4px #DFDFDF; } #debug-bar .muted { - color: #434343; + color: #434343; } #debug-bar .muted td { - color: #dfdfdf; + color: #DFDFDF; } #debug-bar .muted:hover td { - color: #434343; + color: #434343; } #debug-bar #toolbar-position, #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); + filter: brightness(0) invert(0.6); } #debug-bar .ci-label.active { - background-color: #dfdfdf; + background-color: #DFDFDF; } #debug-bar .ci-label:hover { - background-color: #dfdfdf; + background-color: #DFDFDF; } #debug-bar .ci-label .badge { - background-color: #dd4814; - color: #ffffff; + background-color: #DD4814; + color: #FFFFFF; } #debug-bar .tab { - background-color: #ffffff; - box-shadow: 0 -1px 4px #dfdfdf; - -moz-box-shadow: 0 -1px 4px #dfdfdf; - -webkit-box-shadow: 0 -1px 4px #dfdfdf; + background-color: #FFFFFF; + box-shadow: 0 -1px 4px #DFDFDF; + -moz-box-shadow: 0 -1px 4px #DFDFDF; + -webkit-box-shadow: 0 -1px 4px #DFDFDF; } #debug-bar .timeline th, #debug-bar .timeline td { - border-color: #dfdfdf; + border-color: #DFDFDF; } #debug-bar .timeline .timer { - background-color: #dd8615; + background-color: #DD8615; } .debug-view.show-view { - border-color: #dd8615; + border-color: #DD8615; } .debug-view-path { - background-color: #fdc894; - color: #434343; + background-color: #FDC894; + color: #434343; } @media (prefers-color-scheme: dark) { - #debug-icon { - background-color: #252525; - 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 { - color: #dd8615; - } - #debug-bar { - background-color: #252525; - color: #dfdfdf; - } - #debug-bar h1, - #debug-bar h2, - #debug-bar h3, - #debug-bar p, - #debug-bar a, - #debug-bar button, - #debug-bar table, - #debug-bar thead, - #debug-bar tr, - #debug-bar td, - #debug-bar button, - #debug-bar .toolbar { - background-color: transparent; - color: #dfdfdf; - } - #debug-bar button { - background-color: #252525; - } - #debug-bar table strong { - color: #dd8615; - } - #debug-bar table tbody tr:hover { - background-color: #434343; - } - #debug-bar table tbody tr.current { - background-color: #fdc894; - } - #debug-bar table tbody tr.current td { - color: #252525; - } - #debug-bar table tbody tr.current:hover td { - background-color: #dd4814; - color: #ffffff; - } - #debug-bar .toolbar { - background-color: #434343; - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; - } - #debug-bar .toolbar img { - filter: brightness(0) invert(1); - } - #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; - } - #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #434343; - -moz-box-shadow: 0 1px 4px #434343; - -webkit-box-shadow: 0 1px 4px #434343; - } - #debug-bar .muted { - color: #dfdfdf; - } - #debug-bar .muted td { - color: #434343; - } - #debug-bar .muted:hover td { - color: #dfdfdf; - } - #debug-bar #toolbar-position, - #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); - } - #debug-bar .ci-label.active { - background-color: #252525; - } - #debug-bar .ci-label:hover { - background-color: #252525; - } - #debug-bar .ci-label .badge { - background-color: #dd4814; - color: #ffffff; - } - #debug-bar .tab { - background-color: #252525; - box-shadow: 0 -1px 4px #434343; - -moz-box-shadow: 0 -1px 4px #434343; - -webkit-box-shadow: 0 -1px 4px #434343; - } - #debug-bar .timeline th, - #debug-bar .timeline td { - border-color: #434343; - } - #debug-bar .timeline .timer { - background-color: #dd8615; - } - .debug-view.show-view { - border-color: #dd8615; - } - .debug-view-path { - background-color: #fdc894; - color: #434343; - } + #debug-icon { + background-color: #252525; + 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 { + color: #DD8615; + } + #debug-bar { + background-color: #252525; + color: #DFDFDF; + } + #debug-bar h1, + #debug-bar h2, + #debug-bar h3, + #debug-bar p, + #debug-bar a, + #debug-bar button, + #debug-bar table, + #debug-bar thead, + #debug-bar tr, + #debug-bar td, + #debug-bar button, + #debug-bar .toolbar { + background-color: transparent; + color: #DFDFDF; + } + #debug-bar button { + background-color: #252525; + } + #debug-bar table strong { + color: #DD8615; + } + #debug-bar table tbody tr:hover { + background-color: #434343; + } + #debug-bar table tbody tr.current { + background-color: #FDC894; + } + #debug-bar table tbody tr.current td { + color: #252525; + } + #debug-bar table tbody tr.current:hover td { + background-color: #DD4814; + color: #FFFFFF; + } + #debug-bar .toolbar { + background-color: #434343; + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; + } + #debug-bar .toolbar img { + filter: brightness(0) invert(1); + } + #debug-bar.fixed-top .toolbar { + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; + } + #debug-bar.fixed-top .tab { + box-shadow: 0 1px 4px #434343; + -moz-box-shadow: 0 1px 4px #434343; + -webkit-box-shadow: 0 1px 4px #434343; + } + #debug-bar .muted { + color: #DFDFDF; + } + #debug-bar .muted td { + color: #434343; + } + #debug-bar .muted:hover td { + color: #DFDFDF; + } + #debug-bar #toolbar-position, + #debug-bar #toolbar-theme { + filter: brightness(0) invert(0.6); + } + #debug-bar .ci-label.active { + background-color: #252525; + } + #debug-bar .ci-label:hover { + background-color: #252525; + } + #debug-bar .ci-label .badge { + background-color: #DD4814; + color: #FFFFFF; + } + #debug-bar .tab { + background-color: #252525; + box-shadow: 0 -1px 4px #434343; + -moz-box-shadow: 0 -1px 4px #434343; + -webkit-box-shadow: 0 -1px 4px #434343; + } + #debug-bar .timeline th, + #debug-bar .timeline td { + border-color: #434343; + } + #debug-bar .timeline .timer { + background-color: #DD8615; + } + .debug-view.show-view { + border-color: #DD8615; + } + .debug-view-path { + background-color: #FDC894; + color: #434343; + } } #toolbarContainer.dark #debug-icon { - background-color: #252525; - box-shadow: 0 0 4px #dfdfdf; - -moz-box-shadow: 0 0 4px #dfdfdf; - -webkit-box-shadow: 0 0 4px #dfdfdf; + background-color: #252525; + 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 { - color: #dd8615; + color: #DD8615; } #toolbarContainer.dark #debug-bar { - background-color: #252525; - color: #dfdfdf; + background-color: #252525; + color: #DFDFDF; } #toolbarContainer.dark #debug-bar h1, #toolbarContainer.dark #debug-bar h2, @@ -565,109 +559,109 @@ #toolbarContainer.dark #debug-bar td, #toolbarContainer.dark #debug-bar button, #toolbarContainer.dark #debug-bar .toolbar { - background-color: transparent; - color: #dfdfdf; + background-color: transparent; + color: #DFDFDF; } #toolbarContainer.dark #debug-bar button { - background-color: #252525; + background-color: #252525; } #toolbarContainer.dark #debug-bar table strong { - color: #dd8615; + color: #DD8615; } #toolbarContainer.dark #debug-bar table tbody tr:hover { - background-color: #434343; + background-color: #434343; } #toolbarContainer.dark #debug-bar table tbody tr.current { - background-color: #fdc894; + background-color: #FDC894; } #toolbarContainer.dark #debug-bar table tbody tr.current td { - color: #252525; + color: #252525; } #toolbarContainer.dark #debug-bar table tbody tr.current:hover td { - background-color: #dd4814; - color: #ffffff; + background-color: #DD4814; + color: #FFFFFF; } #toolbarContainer.dark #debug-bar .toolbar { - background-color: #434343; - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; + background-color: #434343; + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; } #toolbarContainer.dark #debug-bar .toolbar img { - filter: brightness(0) invert(1); + filter: brightness(0) invert(1); } #toolbarContainer.dark #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; } #toolbarContainer.dark #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #434343; - -moz-box-shadow: 0 1px 4px #434343; - -webkit-box-shadow: 0 1px 4px #434343; + box-shadow: 0 1px 4px #434343; + -moz-box-shadow: 0 1px 4px #434343; + -webkit-box-shadow: 0 1px 4px #434343; } #toolbarContainer.dark #debug-bar .muted { - color: #dfdfdf; + color: #DFDFDF; } #toolbarContainer.dark #debug-bar .muted td { - color: #434343; + color: #434343; } #toolbarContainer.dark #debug-bar .muted:hover td { - color: #dfdfdf; + color: #DFDFDF; } #toolbarContainer.dark #debug-bar #toolbar-position, #toolbarContainer.dark #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); + filter: brightness(0) invert(0.6); } #toolbarContainer.dark #debug-bar .ci-label.active { - background-color: #252525; + background-color: #252525; } #toolbarContainer.dark #debug-bar .ci-label:hover { - background-color: #252525; + background-color: #252525; } #toolbarContainer.dark #debug-bar .ci-label .badge { - background-color: #dd4814; - color: #ffffff; + background-color: #DD4814; + color: #FFFFFF; } #toolbarContainer.dark #debug-bar .tab { - background-color: #252525; - box-shadow: 0 -1px 4px #434343; - -moz-box-shadow: 0 -1px 4px #434343; - -webkit-box-shadow: 0 -1px 4px #434343; + background-color: #252525; + box-shadow: 0 -1px 4px #434343; + -moz-box-shadow: 0 -1px 4px #434343; + -webkit-box-shadow: 0 -1px 4px #434343; } #toolbarContainer.dark #debug-bar .timeline th, #toolbarContainer.dark #debug-bar .timeline td { - border-color: #434343; + border-color: #434343; } #toolbarContainer.dark #debug-bar .timeline .timer { - background-color: #dd8615; + background-color: #DD8615; } #toolbarContainer.dark .debug-view.show-view { - border-color: #dd8615; + border-color: #DD8615; } #toolbarContainer.dark .debug-view-path { - background-color: #fdc894; - color: #434343; + background-color: #FDC894; + color: #434343; } -#toolbarContainer.dark td[data-debugbar-route] input[type="text"] { - background: #000; - color: #fff; +#toolbarContainer.dark td[data-debugbar-route] input[type=text] { + background: #000; + color: #fff; } #toolbarContainer.light #debug-icon { - background-color: #ffffff; - box-shadow: 0 0 4px #dfdfdf; - -moz-box-shadow: 0 0 4px #dfdfdf; - -webkit-box-shadow: 0 0 4px #dfdfdf; + background-color: #FFFFFF; + 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 { - color: #dd8615; + color: #DD8615; } #toolbarContainer.light #debug-bar { - background-color: #ffffff; - color: #434343; + background-color: #FFFFFF; + color: #434343; } #toolbarContainer.light #debug-bar h1, #toolbarContainer.light #debug-bar h2, @@ -681,135 +675,135 @@ #toolbarContainer.light #debug-bar td, #toolbarContainer.light #debug-bar button, #toolbarContainer.light #debug-bar .toolbar { - background-color: transparent; - color: #434343; + background-color: transparent; + color: #434343; } #toolbarContainer.light #debug-bar button { - background-color: #ffffff; + background-color: #FFFFFF; } #toolbarContainer.light #debug-bar table strong { - color: #dd8615; + color: #DD8615; } #toolbarContainer.light #debug-bar table tbody tr:hover { - background-color: #dfdfdf; + background-color: #DFDFDF; } #toolbarContainer.light #debug-bar table tbody tr.current { - background-color: #fdc894; + background-color: #FDC894; } #toolbarContainer.light #debug-bar table tbody tr.current:hover td { - background-color: #dd4814; - color: #ffffff; + background-color: #DD4814; + color: #FFFFFF; } #toolbarContainer.light #debug-bar .toolbar { - background-color: #ffffff; - box-shadow: 0 0 4px #dfdfdf; - -moz-box-shadow: 0 0 4px #dfdfdf; - -webkit-box-shadow: 0 0 4px #dfdfdf; + background-color: #FFFFFF; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; } #toolbarContainer.light #debug-bar .toolbar img { - filter: brightness(0) invert(0.4); + filter: brightness(0) invert(0.4); } #toolbarContainer.light #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #dfdfdf; - -moz-box-shadow: 0 0 4px #dfdfdf; - -webkit-box-shadow: 0 0 4px #dfdfdf; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; } #toolbarContainer.light #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #dfdfdf; - -moz-box-shadow: 0 1px 4px #dfdfdf; - -webkit-box-shadow: 0 1px 4px #dfdfdf; + box-shadow: 0 1px 4px #DFDFDF; + -moz-box-shadow: 0 1px 4px #DFDFDF; + -webkit-box-shadow: 0 1px 4px #DFDFDF; } #toolbarContainer.light #debug-bar .muted { - color: #434343; + color: #434343; } #toolbarContainer.light #debug-bar .muted td { - color: #dfdfdf; + color: #DFDFDF; } #toolbarContainer.light #debug-bar .muted:hover td { - color: #434343; + color: #434343; } #toolbarContainer.light #debug-bar #toolbar-position, #toolbarContainer.light #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); + filter: brightness(0) invert(0.6); } #toolbarContainer.light #debug-bar .ci-label.active { - background-color: #dfdfdf; + background-color: #DFDFDF; } #toolbarContainer.light #debug-bar .ci-label:hover { - background-color: #dfdfdf; + background-color: #DFDFDF; } #toolbarContainer.light #debug-bar .ci-label .badge { - background-color: #dd4814; - color: #ffffff; + background-color: #DD4814; + color: #FFFFFF; } #toolbarContainer.light #debug-bar .tab { - background-color: #ffffff; - box-shadow: 0 -1px 4px #dfdfdf; - -moz-box-shadow: 0 -1px 4px #dfdfdf; - -webkit-box-shadow: 0 -1px 4px #dfdfdf; + background-color: #FFFFFF; + box-shadow: 0 -1px 4px #DFDFDF; + -moz-box-shadow: 0 -1px 4px #DFDFDF; + -webkit-box-shadow: 0 -1px 4px #DFDFDF; } #toolbarContainer.light #debug-bar .timeline th, #toolbarContainer.light #debug-bar .timeline td { - border-color: #dfdfdf; + border-color: #DFDFDF; } #toolbarContainer.light #debug-bar .timeline .timer { - background-color: #dd8615; + background-color: #DD8615; } #toolbarContainer.light .debug-view.show-view { - border-color: #dd8615; + border-color: #DD8615; } #toolbarContainer.light .debug-view-path { - background-color: #fdc894; - color: #434343; + background-color: #FDC894; + color: #434343; } .debug-bar-width30 { - width: 30%; + width: 30%; } .debug-bar-width10 { - width: 10%; + width: 10%; } .debug-bar-width70p { - width: 70px; + width: 70px; } .debug-bar-width190p { - width: 190px; + width: 190px; } .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; + overflow: hidden; } /* ENDLESS ROTATE */ .rotate { - animation: rotate 9s linear infinite; + animation: rotate 9s linear infinite; } @keyframes rotate { - to { - transform: rotate(360deg); - } + to { + transform: rotate(360deg); + } } From b1a2e1db503d8808486b0528b31967968d06376b Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sat, 24 Jun 2023 23:53:20 -0500 Subject: [PATCH 258/485] Suppress Psalm errors on IteratorFilter --- system/HotReloader/IteratorFilter.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/system/HotReloader/IteratorFilter.php b/system/HotReloader/IteratorFilter.php index 753cfdf8b1a9..7f40a464d0ac 100644 --- a/system/HotReloader/IteratorFilter.php +++ b/system/HotReloader/IteratorFilter.php @@ -17,8 +17,7 @@ /** * @internal * - * @extends RecursiveFilterIterator - * @implements RecursiveIterator + * @psalm-suppress MissingTemplateParam */ final class IteratorFilter extends RecursiveFilterIterator implements RecursiveIterator { From fc5bd0290b90ef916365b954dda152280b0f4843 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 25 Jun 2023 00:01:58 -0500 Subject: [PATCH 259/485] Add missing group to test --- tests/system/HotReloader/DirectoryHasherTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/system/HotReloader/DirectoryHasherTest.php b/tests/system/HotReloader/DirectoryHasherTest.php index 08f2ea6eef2a..f5de5da523d5 100644 --- a/tests/system/HotReloader/DirectoryHasherTest.php +++ b/tests/system/HotReloader/DirectoryHasherTest.php @@ -16,6 +16,8 @@ /** * @internal + * + * @group Others */ final class DirectoryHasherTest extends CIUnitTestCase { From ac761f0a12df8ae88675b530bb2cb429eecc3393 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 25 Jun 2023 23:34:19 -0500 Subject: [PATCH 260/485] Apply suggestions from code review Co-authored-by: kenjis --- user_guide_src/source/testing/debugging.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/testing/debugging.rst b/user_guide_src/source/testing/debugging.rst index 138bf49af2bf..6c589ead52aa 100644 --- a/user_guide_src/source/testing/debugging.rst +++ b/user_guide_src/source/testing/debugging.rst @@ -178,10 +178,12 @@ outer array's key is the name of the section on the Vars tab: Hot Reloading ============= +.. versionadded:: 4.4.0 + The Debug Toolbar includes a feature called Hot Reloading that allows you to make changes to your application's code and have them automatically reloaded in the browser without having to refresh the page. This is a great time-saver during development. To enable Hot Reloading while you are developing, you can click the button on the left side of the toolbar that looks like a refresh icon. This will enable Hot Reloading for all pages until you disable it. -Hot Reloading works by scanning the files within the ``app`` directory every second and looking for changes. If it finds any, it will send a message to the browser to reload the page. It does not scan any other directories, so if you are making changes to files outside of the ``app`` directory, you will need to manually refresh the page. +Hot Reloading works by scanning the files within the **app** directory every second and looking for changes. If it finds any, it will send a message to the browser to reload the page. It does not scan any other directories, so if you are making changes to files outside of the **app** directory, you will need to manually refresh the page. -If you need to watch files outside of the ``app`` directory, or are finding it slow due to the size of your project, you can specify the directories to scan and the file extensions to scan for in the ``$toolbarRefreshDirs`` and ``$toolbarRefreshExtensions`` properties of the **app/Config/Toolbar.php** configuration file. +If you need to watch files outside of the **app** directory, or are finding it slow due to the size of your project, you can specify the directories to scan and the file extensions to scan for in the ``$watchedDirectories`` and ``$watchedExtensions`` properties of the **app/Config/Toolbar.php** configuration file. From 3b67f49045d2f708942e73da0de557dc61c6e55f Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 25 Jun 2023 23:35:42 -0500 Subject: [PATCH 261/485] Categorize the changelog entry for hot reloading --- user_guide_src/source/changelogs/v4.4.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 432e5e12ae20..a082ba483447 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -56,8 +56,6 @@ Method Signature Changes Enhancements ************ -- The Debug Toolbar now has a new "Hot Reload" feature that can be used to automatically reload the page when a file is changed. See :ref:`debug-toolbar-hot-reload`. - Commands ======== @@ -67,6 +65,8 @@ Commands Testing ======= +- The Debug Toolbar now has a new "Hot Reload" feature that can be used to automatically reload the page when a file is changed. See :ref:`debug-toolbar-hot-reload`. + Database ======== From e8be829abdd4bb01841983d54aac9aaed29a89c5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 26 Jun 2023 14:52:17 +0900 Subject: [PATCH 262/485] refactor: use ::class to config() param --- system/Config/Services.php | 3 ++- system/HotReloader/DirectoryHasher.php | 3 ++- system/HotReloader/IteratorFilter.php | 3 ++- system/Security/Security.php | 3 +-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/system/Config/Services.php b/system/Config/Services.php index e337a63b8bb5..89a49b4bf468 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -76,6 +76,7 @@ use Config\Modules; use Config\Pager as PagerConfig; use Config\Paths; +use Config\Routing; use Config\Services as AppServices; use Config\Session as SessionConfig; use Config\Toolbar as ToolbarConfig; @@ -599,7 +600,7 @@ public static function routes(bool $getShared = true) return static::getSharedInstance('routes'); } - return new RouteCollection(AppServices::locator(), config(Modules::class), config('Routing')); + return new RouteCollection(AppServices::locator(), config(Modules::class), config(Routing::class)); } /** diff --git a/system/HotReloader/DirectoryHasher.php b/system/HotReloader/DirectoryHasher.php index fac072fdb24a..3d9914d8c8f8 100644 --- a/system/HotReloader/DirectoryHasher.php +++ b/system/HotReloader/DirectoryHasher.php @@ -12,6 +12,7 @@ namespace CodeIgniter\HotReloader; use CodeIgniter\Exceptions\FrameworkException; +use Config\Toolbar; use FilesystemIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -40,7 +41,7 @@ public function hashApp(): array { $hashes = []; - $watchedDirectories = config('Toolbar')->watchedDirectories; + $watchedDirectories = config(Toolbar::class)->watchedDirectories; foreach ($watchedDirectories as $directory) { if (is_dir(ROOTPATH . $directory)) { diff --git a/system/HotReloader/IteratorFilter.php b/system/HotReloader/IteratorFilter.php index 7f40a464d0ac..db775f90061e 100644 --- a/system/HotReloader/IteratorFilter.php +++ b/system/HotReloader/IteratorFilter.php @@ -11,6 +11,7 @@ namespace CodeIgniter\HotReloader; +use Config\Toolbar; use RecursiveFilterIterator; use RecursiveIterator; @@ -27,7 +28,7 @@ public function __construct(RecursiveIterator $iterator) { parent::__construct($iterator); - $this->watchedExtensions = config('Toolbar')->watchedExtensions; + $this->watchedExtensions = config(Toolbar::class)->watchedExtensions; } /** diff --git a/system/Security/Security.php b/system/Security/Security.php index 29a9314a2e5d..d7a5d49c6f09 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -194,8 +194,7 @@ public function __construct(App $config) } if ($this->isCSRFCookie()) { - /** @var CookieConfig $cookie */ - $cookie = config('Cookie'); + $cookie = config(CookieConfig::class); $this->configureCookie($cookie); } else { From c8aec65e79d9426f2e4e106b4eabf924a3b688bb Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 26 Jun 2023 15:37:04 +0900 Subject: [PATCH 263/485] refactor: remove deprecated Request::$proxyIPs --- system/HTTP/Request.php | 16 +--------------- system/HTTP/RequestTrait.php | 13 ++----------- 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index ebe602b18e38..d6eedf89c2ac 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -21,15 +21,6 @@ class Request extends OutgoingRequest implements RequestInterface { use RequestTrait; - /** - * Proxy IPs - * - * @var array - * - * @deprecated Check the App config directly - */ - protected $proxyIPs; - /** * Constructor. * @@ -37,13 +28,8 @@ class Request extends OutgoingRequest implements RequestInterface * * @deprecated The $config is no longer needed and will be removed in a future version */ - public function __construct($config = null) + public function __construct($config = null) // @phpstan-ignore-line { - /** - * @deprecated $this->proxyIps property will be removed in the future - */ - $this->proxyIPs = $config->proxyIPs; - if (empty($this->method)) { $this->method = $this->getServer('REQUEST_METHOD') ?? 'GET'; } diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index 57f97982dea4..32b0401bbbe9 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -60,17 +60,8 @@ public function getIPAddress(): string 'valid_ip', ]; - /** - * @deprecated $this->proxyIPs property will be removed in the future - */ - // @phpstan-ignore-next-line - $proxyIPs = $this->proxyIPs ?? config(App::class)->proxyIPs; - // @phpstan-ignore-next-line - - // Workaround for old Config\App file. App::$proxyIPs may be empty string. - if ($proxyIPs === '') { - $proxyIPs = []; - } + $proxyIPs = config(App::class)->proxyIPs; + if (! empty($proxyIPs) && (! is_array($proxyIPs) || is_int(array_key_first($proxyIPs)))) { throw new ConfigException( 'You must set an array with Proxy IP address key and HTTP header name value in Config\App::$proxyIPs.' From fe0b5f5c0cf1e53b4a142718f73b38973fbe9060 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 26 Jun 2023 15:38:00 +0900 Subject: [PATCH 264/485] docs: add changelog and upgrade guide --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + user_guide_src/source/installation/upgrade_440.rst | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 2d8cbf5c73e1..ee2d31728b8d 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -137,6 +137,7 @@ Changes - **Config:** The deprecated Session items in **app/Config/App.php** has been removed. - **Config:** Routing settings have been moved to **app/Config/Routing.php** config file. See :ref:`Upgrading Guide `. +- **Request:** The deprecated property ``$proxyIPs`` has been removed. - **DownloadResponse:** When generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. - **Autoloader:** Before v4.4.0, CodeIgniter autoloader did not allow special characters that are illegal in filenames on certain operating systems. diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 9c765f24434b..586097a9cc30 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -72,6 +72,12 @@ Mandatory File Changes Config Files ============ +app/Config/App.php +------------------ + +- The property ``$proxyIPs`` must be an array. If you don't use proxy servers, + it must be ``public array $proxyIPs = [];``. + .. _upgrade-440-config-routing: app/Config/Routing.php From 38a2e4d23ac5a5ed58a27e750b9bfeb38bab0c8a Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 26 Jun 2023 15:48:26 +0900 Subject: [PATCH 265/485] test: update test code --- tests/system/HTTP/IncomingRequestTest.php | 16 +++++++++++----- tests/system/HTTP/RequestTest.php | 7 +++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 2b61850ec579..2c1f7017a610 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Config\Factories; use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\Files\UploadedFile; @@ -973,7 +974,8 @@ public function testGetIPAddressThruProxy() '10.0.1.200' => 'X-Forwarded-For', '192.168.5.0/24' => 'X-Forwarded-For', ]; - $this->request = new Request($config); + Factories::injectMock('config', App::class, $config); + $this->request = new Request(); $this->request->populateHeaders(); // we should see the original forwarded address @@ -990,7 +992,8 @@ public function testGetIPAddressThruProxyIPv6() $config->proxyIPs = [ '2001:db8::2:1' => 'X-Forwarded-For', ]; - $this->request = new Request($config); + Factories::injectMock('config', App::class, $config); + $this->request = new Request(); $this->request->populateHeaders(); // we should see the original forwarded address @@ -1075,7 +1078,8 @@ public function testGetIPAddressThruProxySubnet() $config = new App(); $config->proxyIPs = ['192.168.5.0/24' => 'X-Forwarded-For']; - $this->request = new Request($config); + Factories::injectMock('config', App::class, $config); + $this->request = new Request(); $this->request->populateHeaders(); // we should see the original forwarded address @@ -1090,7 +1094,8 @@ public function testGetIPAddressThruProxySubnetIPv6() $config = new App(); $config->proxyIPs = ['2001:db8:1234::/48' => 'X-Forwarded-For']; - $this->request = new Request($config); + Factories::injectMock('config', App::class, $config); + $this->request = new Request(); $this->request->populateHeaders(); // we should see the original forwarded address @@ -1166,7 +1171,8 @@ public function testGetIPAddressThruProxyInvalidConfigArray() $config = new App(); $config->proxyIPs = ['192.168.5.0/28']; - $this->request = new Request($config); + Factories::injectMock('config', App::class, $config); + $this->request = new Request(); $this->request->populateHeaders(); $this->request->getIPAddress(); diff --git a/tests/system/HTTP/RequestTest.php b/tests/system/HTTP/RequestTest.php index 17cf45078fe0..4b54577d18ec 100644 --- a/tests/system/HTTP/RequestTest.php +++ b/tests/system/HTTP/RequestTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Config\Factories; use CodeIgniter\Test\CIUnitTestCase; use Config\App; @@ -616,7 +617,8 @@ public function testGetIPAddressThruProxy() '10.0.1.200' => 'X-Forwarded-For', '192.168.5.0/24' => 'X-Forwarded-For', ]; - $this->request = new Request($config); + Factories::injectMock('config', App::class, $config); + $this->request = new Request(); $this->request->populateHeaders(); // we should see the original forwarded address @@ -667,7 +669,8 @@ public function testGetIPAddressThruProxySubnet() $config = new App(); $config->proxyIPs = ['192.168.5.0/24' => 'X-Forwarded-For']; - $this->request = new Request($config); + Factories::injectMock('config', App::class, $config); + $this->request = new Request(); $this->request->populateHeaders(); // we should see the original forwarded address From 55b57df25f0848a5968962611f133e93a6a9815b Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 26 Jun 2023 20:37:41 +0900 Subject: [PATCH 266/485] chore: add system/HTTP/Request.php to skip --- rector.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rector.php b/rector.php index 238f94307649..1532d33471a3 100644 --- a/rector.php +++ b/rector.php @@ -92,6 +92,8 @@ __DIR__ . '/system/Debug/Exceptions.php', // @TODO remove if deprecated $httpVerb is removed __DIR__ . '/system/Router/AutoRouterImproved.php', + // @TODO remove if deprecated $config is removed + __DIR__ . '/system/HTTP/Request.php', ], // check on constant compare From 970899674cc65d4ed41e7ea5dceda708bae9cde7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 07:12:50 +0900 Subject: [PATCH 267/485] refactor: restore deprecated $proxyIPs --- system/HTTP/Request.php | 9 +++++++++ user_guide_src/source/changelogs/v4.4.0.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index d6eedf89c2ac..be15fc774835 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -21,6 +21,15 @@ class Request extends OutgoingRequest implements RequestInterface { use RequestTrait; + /** + * Proxy IPs + * + * @var array + * + * @deprecated 4.0.5 No longer used. Check the App config directly + */ + protected $proxyIPs; + /** * Constructor. * diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index ee2d31728b8d..2d8cbf5c73e1 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -137,7 +137,6 @@ Changes - **Config:** The deprecated Session items in **app/Config/App.php** has been removed. - **Config:** Routing settings have been moved to **app/Config/Routing.php** config file. See :ref:`Upgrading Guide `. -- **Request:** The deprecated property ``$proxyIPs`` has been removed. - **DownloadResponse:** When generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. - **Autoloader:** Before v4.4.0, CodeIgniter autoloader did not allow special characters that are illegal in filenames on certain operating systems. From e9d9026a6f046c595a87511cd7eb2116d957bbdb Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 07:13:26 +0900 Subject: [PATCH 268/485] docs: add versions for @deprecated --- system/HTTP/Request.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index be15fc774835..26b7b8f460c8 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -35,7 +35,7 @@ class Request extends OutgoingRequest implements RequestInterface * * @param App $config * - * @deprecated The $config is no longer needed and will be removed in a future version + * @deprecated 4.0.5 The $config is no longer needed and will be removed in a future version */ public function __construct($config = null) // @phpstan-ignore-line { @@ -54,7 +54,7 @@ public function __construct($config = null) // @phpstan-ignore-line * @param string $ip IP Address * @param string $which IP protocol: 'ipv4' or 'ipv6' * - * @deprecated Use Validation instead + * @deprecated 4.0.5 Use Validation instead * * @codeCoverageIgnore */ @@ -68,7 +68,7 @@ public function isValidIP(?string $ip = null, ?string $which = null): bool * * @param bool $upper Whether to return in upper or lower case. * - * @deprecated The $upper functionality will be removed and this will revert to its PSR-7 equivalent + * @deprecated 4.0.5 The $upper functionality will be removed and this will revert to its PSR-7 equivalent * * @codeCoverageIgnore */ @@ -82,7 +82,7 @@ public function getMethod(bool $upper = false): string * * @return $this * - * @deprecated Use withMethod() instead for immutability + * @deprecated 4.0.5 Use withMethod() instead for immutability * * @codeCoverageIgnore */ From a95676570dcf747f2fa82c9da56da0a1893b64dd Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 25 Jan 2023 09:56:29 +0900 Subject: [PATCH 269/485] docs: fix PHPDoc I found another type data in the $route array. --- system/Router/RouteCollection.php | 1 + 1 file changed, 1 insertion(+) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index f535d392817c..fe3f2e99fa82 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -119,6 +119,7 @@ class RouteCollection implements RouteCollectionInterface * routeName => [ * 'route' => [ * routeKey(regex) => handler, + * or routeKey(regex)(from) => [routeKey(regex)(to) => handler], // redirect * ], * 'redirect' => statusCode, * ] From e0864515fdb8930c92f4909c97c2022f5d4e7346 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 25 Jan 2023 10:42:38 +0900 Subject: [PATCH 270/485] refactor: stop reassignment --- system/Router/RouteCollection.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index fe3f2e99fa82..c111804e0ea4 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -640,12 +640,14 @@ public function addRedirect(string $from, string $to, int $status = 302) { // Use the named route's pattern if this is a named route. if (array_key_exists($to, $this->routes['*'])) { - $to = $this->routes['*'][$to]['route']; + $redirectTo = $this->routes['*'][$to]['route']; } elseif (array_key_exists($to, $this->routes['get'])) { - $to = $this->routes['get'][$to]['route']; + $redirectTo = $this->routes['get'][$to]['route']; + } else { + $redirectTo = $to; } - $this->create('*', $from, $to, ['redirect' => $status]); + $this->create('*', $from, $redirectTo, ['redirect' => $status]); return $this; } From aa569317d786b6fc9f173121a6b674c54362aaf7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 25 Jan 2023 11:07:00 +0900 Subject: [PATCH 271/485] refactor: add variable To distinguish between from and routeKey. --- system/Router/RouteCollection.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index c111804e0ea4..08997a7bd6e6 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1388,10 +1388,11 @@ protected function create(string $verb, string $from, $to, ?array $options = nul } } + $routeKey = $from; // Replace our regex pattern placeholders with the actual thing // so that the Router doesn't need to know about any of this. foreach ($this->placeholders as $tag => $pattern) { - $from = str_ireplace(':' . $tag, $pattern, $from); + $routeKey = str_ireplace(':' . $tag, $pattern, $routeKey); } // If is redirect, No processing @@ -1406,7 +1407,7 @@ protected function create(string $verb, string $from, $to, ?array $options = nul $to = '\\' . ltrim($to, '\\'); } - $name = $options['as'] ?? $from; + $name = $options['as'] ?? $routeKey; helper('array'); @@ -1415,16 +1416,16 @@ protected function create(string $verb, string $from, $to, ?array $options = nul // routes should always be the "source of truth". // this works only because discovered routes are added just prior // to attempting to route the request. - $fromExists = dot_array_search('*.route.' . $from, $this->routes[$verb] ?? []) !== null; + $fromExists = dot_array_search('*.route.' . $routeKey, $this->routes[$verb] ?? []) !== null; if ((isset($this->routes[$verb][$name]) || $fromExists) && ! $overwrite) { return; } $this->routes[$verb][$name] = [ - 'route' => [$from => $to], + 'route' => [$routeKey => $to], ]; - $this->routesOptions[$verb][$from] = $options; + $this->routesOptions[$verb][$routeKey] = $options; // Is this a redirect? if (isset($options['redirect']) && is_numeric($options['redirect'])) { From 2af286417e271576ea6d192aa73735d2e12a045e Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 25 Jan 2023 11:28:12 +0900 Subject: [PATCH 272/485] refactor: rename parameter name --- system/Router/RouteCollection.php | 12 ++++++++---- system/Router/RouteCollectionInterface.php | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 08997a7bd6e6..6c3a7bc776e3 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -654,12 +654,14 @@ public function addRedirect(string $from, string $to, int $status = 302) /** * Determines if the route is a redirecting route. + * + * @param string $routeKey routeKey or route name */ - public function isRedirect(string $from): bool + public function isRedirect(string $routeKey): bool { foreach ($this->routes['*'] as $name => $route) { // Named route? - if ($name === $from || key($route['route']) === $from) { + if ($name === $routeKey || key($route['route']) === $routeKey) { return isset($route['redirect']) && is_numeric($route['redirect']); } } @@ -669,12 +671,14 @@ public function isRedirect(string $from): bool /** * Grabs the HTTP status code from a redirecting Route. + * + * @param string $routeKey routeKey or route name */ - public function getRedirectCode(string $from): int + public function getRedirectCode(string $routeKey): int { foreach ($this->routes['*'] as $name => $route) { // Named route? - if ($name === $from || key($route['route']) === $from) { + if ($name === $routeKey || key($route['route']) === $routeKey) { return $route['redirect'] ?? 0; } } diff --git a/system/Router/RouteCollectionInterface.php b/system/Router/RouteCollectionInterface.php index 701e893dcd16..0ca43523a89b 100644 --- a/system/Router/RouteCollectionInterface.php +++ b/system/Router/RouteCollectionInterface.php @@ -179,12 +179,12 @@ public function reverseRoute(string $search, ...$params); /** * Determines if the route is a redirecting route. */ - public function isRedirect(string $from): bool; + public function isRedirect(string $routeKey): bool; /** * Grabs the HTTP status code from a redirecting Route. */ - public function getRedirectCode(string $from): int; + public function getRedirectCode(string $routeKey): int; /** * Get the flag that limit or not the routes with {locale} placeholder to App::$supportedLocales From 26be1f2c138eb1ee38cf0f1c8c2557f31a61488e Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 25 Jan 2023 12:09:45 +0900 Subject: [PATCH 273/485] refactor: rename parameter and variable name --- system/Router/RouteCollection.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 6c3a7bc776e3..5ed06fb0eb26 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1114,8 +1114,8 @@ public function reverseRoute(string $search, ...$params) // all routes to find a match. foreach ($this->routes as $collection) { foreach ($collection as $route) { - $from = key($route['route']); - $to = $route['route'][$from]; + $routeKey = key($route['route']); + $to = $route['route'][$routeKey]; // ignore closures if (! is_string($to)) { @@ -1139,7 +1139,7 @@ public function reverseRoute(string $search, ...$params) continue; } - return $this->buildReverseRoute($from, $params); + return $this->buildReverseRoute($routeKey, $params); } } @@ -1254,21 +1254,21 @@ protected function fillRouteParams(string $from, ?array $params = null): string * @param array $params One or more parameters to be passed to the route. * The last parameter allows you to set the locale. */ - protected function buildReverseRoute(string $from, array $params): string + protected function buildReverseRoute(string $routeKey, array $params): string { $locale = null; // Find all of our back-references in the original route - preg_match_all('/\(([^)]+)\)/', $from, $matches); + preg_match_all('/\(([^)]+)\)/', $routeKey, $matches); if (empty($matches[0])) { - if (strpos($from, '{locale}') !== false) { + if (strpos($routeKey, '{locale}') !== false) { $locale = $params[0] ?? null; } - $from = $this->replaceLocale($from, $locale); + $routeKey = $this->replaceLocale($routeKey, $locale); - return '/' . ltrim($from, '/'); + return '/' . ltrim($routeKey, '/'); } // Locale is passed? @@ -1286,13 +1286,13 @@ protected function buildReverseRoute(string $from, array $params): string // Ensure that the param we're inserting matches // the expected param type. - $pos = strpos($from, $pattern); - $from = substr_replace($from, $params[$index], $pos, strlen($pattern)); + $pos = strpos($routeKey, $pattern); + $routeKey = substr_replace($routeKey, $params[$index], $pos, strlen($pattern)); } - $from = $this->replaceLocale($from, $locale); + $routeKey = $this->replaceLocale($routeKey, $locale); - return '/' . ltrim($from, '/'); + return '/' . ltrim($routeKey, '/'); } /** From 7c125b90cf2b2614ae24d1c93b2686c398dcb9fc Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 25 Jan 2023 14:59:58 +0900 Subject: [PATCH 274/485] perf: change RouteCollection::$routes structure --- system/Router/RouteCollection.php | 125 ++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 40 deletions(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 5ed06fb0eb26..b38844bbcdd8 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -116,13 +116,16 @@ class RouteCollection implements RouteCollectionInterface * * [ * verb => [ - * routeName => [ - * 'route' => [ - * routeKey(regex) => handler, - * or routeKey(regex)(from) => [routeKey(regex)(to) => handler], // redirect - * ], + * routeKey(regex) => [ + * 'name' => routeName + * 'handler' => handler, + * ], + * // redirect route + * or routeKey(regex)(from) => [ + * 'name' => routeName + * 'handler' => [routeKey(regex)(to) => handler], * 'redirect' => statusCode, - * ] + * ], * ], * ] */ @@ -139,6 +142,30 @@ class RouteCollection implements RouteCollectionInterface 'cli' => [], ]; + /** + * Array of routes names + * + * @var array + * + * [ + * verb => [ + * routeName => routeKey(regex) + * ], + * ] + */ + protected $routesNames = [ + '*' => [], + 'options' => [], + 'get' => [], + 'head' => [], + 'post' => [], + 'put' => [], + 'delete' => [], + 'trace' => [], + 'connect' => [], + 'cli' => [], + ]; + /** * Array of routes options * @@ -538,9 +565,8 @@ public function getRoutes(?string $verb = null): array // before any of the generic, "add" routes. $collection = $this->routes[$verb] + ($this->routes['*'] ?? []); - foreach ($collection as $r) { - $key = key($r['route']); - $routes[$key] = $r['route'][$key]; + foreach ($collection as $routeKey => $r) { + $routes[$routeKey] = $r['handler']; } } @@ -639,11 +665,16 @@ public function add(string $from, $to, ?array $options = null): RouteCollectionI public function addRedirect(string $from, string $to, int $status = 302) { // Use the named route's pattern if this is a named route. - if (array_key_exists($to, $this->routes['*'])) { - $redirectTo = $this->routes['*'][$to]['route']; - } elseif (array_key_exists($to, $this->routes['get'])) { - $redirectTo = $this->routes['get'][$to]['route']; + if (array_key_exists($to, $this->routesNames['*'])) { + $routeName = $to; + $routeKey = $this->routesNames['*'][$routeName]; + $redirectTo = [$routeKey => $this->routes['*'][$routeKey]['handler']]; + } elseif (array_key_exists($to, $this->routesNames['get'])) { + $routeName = $to; + $routeKey = $this->routesNames['get'][$routeName]; + $redirectTo = [$routeKey => $this->routes['get'][$routeKey]['handler']]; } else { + // The named route is not found. $redirectTo = $to; } @@ -659,11 +690,16 @@ public function addRedirect(string $from, string $to, int $status = 302) */ public function isRedirect(string $routeKey): bool { - foreach ($this->routes['*'] as $name => $route) { - // Named route? - if ($name === $routeKey || key($route['route']) === $routeKey) { - return isset($route['redirect']) && is_numeric($route['redirect']); - } + if (isset($this->routes['*'][$routeKey]['redirect'])) { + return true; + } + + // This logic is not used. Should be deprecated? + $routeName = $this->routes['*'][$routeKey]['name'] ?? null; + if ($routeName === $routeKey) { + $routeKey = $this->routesNames['*'][$routeName]; + + return isset($this->routes['*'][$routeKey]['redirect']); } return false; @@ -676,11 +712,16 @@ public function isRedirect(string $routeKey): bool */ public function getRedirectCode(string $routeKey): int { - foreach ($this->routes['*'] as $name => $route) { - // Named route? - if ($name === $routeKey || key($route['route']) === $routeKey) { - return $route['redirect'] ?? 0; - } + if (isset($this->routes['*'][$routeKey]['redirect'])) { + return $this->routes['*'][$routeKey]['redirect']; + } + + // This logic is not used. Should be deprecated? + $routeName = $this->routes['*'][$routeKey]['name'] ?? null; + if ($routeName === $routeKey) { + $routeKey = $this->routesNames['*'][$routeName]; + + return $this->routes['*'][$routeKey]['redirect']; } return 0; @@ -1095,9 +1136,11 @@ public function environment(string $env, Closure $callback): RouteCollectionInte public function reverseRoute(string $search, ...$params) { // Named routes get higher priority. - foreach ($this->routes as $collection) { + foreach ($this->routesNames as $collection) { if (array_key_exists($search, $collection)) { - return $this->buildReverseRoute(key($collection[$search]['route']), $params); + $routeKey = $collection[$search]; + + return $this->buildReverseRoute($routeKey, $params); } } @@ -1113,9 +1156,8 @@ public function reverseRoute(string $search, ...$params) // If it's not a named route, then loop over // all routes to find a match. foreach ($this->routes as $collection) { - foreach ($collection as $route) { - $routeKey = key($route['route']); - $to = $route['route'][$routeKey]; + foreach ($collection as $routeKey => $route) { + $to = $route['handler']; // ignore closures if (! is_string($to)) { @@ -1340,7 +1382,7 @@ protected function create(string $verb, string $from, $to, ?array $options = nul } // When redirecting to named route, $to is an array like `['zombies' => '\Zombies::index']`. - if (is_array($to) && count($to) === 2) { + if (is_array($to) && isset($to[0])) { $to = $this->processArrayCallableSyntax($from, $to); } @@ -1420,20 +1462,21 @@ protected function create(string $verb, string $from, $to, ?array $options = nul // routes should always be the "source of truth". // this works only because discovered routes are added just prior // to attempting to route the request. - $fromExists = dot_array_search('*.route.' . $routeKey, $this->routes[$verb] ?? []) !== null; - if ((isset($this->routes[$verb][$name]) || $fromExists) && ! $overwrite) { + $fromExists = isset($this->routes[$verb][$routeKey]); + if ((isset($this->routesNames[$verb][$name]) || $fromExists) && ! $overwrite) { return; } - $this->routes[$verb][$name] = [ - 'route' => [$routeKey => $to], + $this->routes[$verb][$routeKey] = [ + 'name' => $name, + 'handler' => $to, ]; - $this->routesOptions[$verb][$routeKey] = $options; + $this->routesNames[$verb][$name] = $routeKey; // Is this a redirect? if (isset($options['redirect']) && is_numeric($options['redirect'])) { - $this->routes['*'][$name]['redirect'] = $options['redirect']; + $this->routes['*'][$routeKey]['redirect'] = $options['redirect']; } } @@ -1574,12 +1617,15 @@ private function determineCurrentSubdomain() */ public function resetRoutes() { - $this->routes = ['*' => []]; + $this->routes = $this->routesNames = ['*' => []]; foreach ($this->defaultHTTPMethods as $verb) { - $this->routes[$verb] = []; + $this->routes[$verb] = []; + $this->routesNames[$verb] = []; } + $this->routesOptions = []; + $this->prioritizeDetected = false; $this->didDiscover = false; } @@ -1645,9 +1691,8 @@ public function getRegisteredControllers(?string $verb = '*'): array if ($verb === '*') { foreach ($this->defaultHTTPMethods as $tmpVerb) { - foreach ($this->routes[$tmpVerb] as $route) { - $routeKey = key($route['route']); - $controller = $this->getControllerName($route['route'][$routeKey]); + foreach ($this->routes[$tmpVerb] as $routeKey => $route) { + $controller = $this->getControllerName($route['handler']); if ($controller !== null) { $controllers[] = $controller; } From 579de5477c53987141c324fc36bc00e99c99b5ab Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 25 Jan 2023 17:37:20 +0900 Subject: [PATCH 275/485] refactor: remove unused variable --- system/Router/RouteCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index b38844bbcdd8..03078395b4d2 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1691,7 +1691,7 @@ public function getRegisteredControllers(?string $verb = '*'): array if ($verb === '*') { foreach ($this->defaultHTTPMethods as $tmpVerb) { - foreach ($this->routes[$tmpVerb] as $routeKey => $route) { + foreach ($this->routes[$tmpVerb] as $route) { $controller = $this->getControllerName($route['handler']); if ($controller !== null) { $controllers[] = $controller; From fea7e9a1ee0d690dae81536de69aafd616867ca1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 26 Jan 2023 08:04:09 +0900 Subject: [PATCH 276/485] refactor: rename variable name --- system/Router/RouteCollection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 03078395b4d2..d194d474a599 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1462,8 +1462,8 @@ protected function create(string $verb, string $from, $to, ?array $options = nul // routes should always be the "source of truth". // this works only because discovered routes are added just prior // to attempting to route the request. - $fromExists = isset($this->routes[$verb][$routeKey]); - if ((isset($this->routesNames[$verb][$name]) || $fromExists) && ! $overwrite) { + $routeKeyExists = isset($this->routes[$verb][$routeKey]); + if ((isset($this->routesNames[$verb][$name]) || $routeKeyExists) && ! $overwrite) { return; } From 3851f414918ef282742cefd54c4256b5048a02ac Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 30 Apr 2023 12:08:43 +0900 Subject: [PATCH 277/485] feat: add $from for future use --- system/Router/RouteCollection.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index d194d474a599..865c8aa696f7 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -119,12 +119,14 @@ class RouteCollection implements RouteCollectionInterface * routeKey(regex) => [ * 'name' => routeName * 'handler' => handler, + * 'from' => from, * ], * // redirect route * or routeKey(regex)(from) => [ * 'name' => routeName * 'handler' => [routeKey(regex)(to) => handler], * 'redirect' => statusCode, + * 'from' => from, * ], * ], * ] @@ -1470,6 +1472,7 @@ protected function create(string $verb, string $from, $to, ?array $options = nul $this->routes[$verb][$routeKey] = [ 'name' => $name, 'handler' => $to, + 'from' => $from, ]; $this->routesOptions[$verb][$routeKey] = $options; $this->routesNames[$verb][$name] = $routeKey; From 0c7a2bf48a01f16bdc8da8c9b350093f4eda9d14 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 30 Apr 2023 12:09:07 +0900 Subject: [PATCH 278/485] docs: update doc comment --- system/Router/RouteCollection.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 865c8aa696f7..030edf8f9ca1 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -117,16 +117,18 @@ class RouteCollection implements RouteCollectionInterface * [ * verb => [ * routeKey(regex) => [ - * 'name' => routeName - * 'handler' => handler, - * 'from' => from, + * 'name' => routeName + * 'handler' => handler, + * 'from' => from, * ], - * // redirect route - * or routeKey(regex)(from) => [ + * ], + * // redirect route + * '*' => [ + * routeKey(regex)(from) => [ * 'name' => routeName * 'handler' => [routeKey(regex)(to) => handler], + * 'from' => from, * 'redirect' => statusCode, - * 'from' => from, * ], * ], * ] From df8556ee1bc8d980e0cbdcf121c12c213cd2f6ca Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 09:40:25 +0900 Subject: [PATCH 279/485] docs: add changelog and upgrade note --- user_guide_src/source/changelogs/v4.4.0.rst | 2 ++ user_guide_src/source/installation/upgrade_440.rst | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 2d8cbf5c73e1..0e5db5e1fc23 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -144,6 +144,8 @@ Changes So if you installed CodeIgniter under the folder that contains the special characters like ``(``, ``)``, etc., CodeIgniter didn't work. Since v4.4.0, this restriction has been removed. +- **RouteCollection:** The array structure of the protected property ``$routes`` + has been modified for performance. Deprecations ************ diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 9c765f24434b..a0c34f67e77b 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -66,6 +66,15 @@ Interface Changes Some interface changes have been made. Classes that implement them should update their APIs to reflect the changes. See :ref:`v440-interface-changes` for details. +RouteCollection::$routes +======================== + +The array structure of the protected property ``$routes`` has been modified for +performance. + +If you extend ``RouteCollection`` and use the ``$routes``, update your code to +match the new array structure. + Mandatory File Changes ********************** From 818b1030c805c4d65621850e9168bde2aa9eae82 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 09:54:26 +0900 Subject: [PATCH 280/485] docs: fix RST format --- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 2d8cbf5c73e1..e340d33cc667 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -154,7 +154,7 @@ Deprecations ``ExceptionHandler``. - **Autoloader:** ``Autoloader::sanitizeFilename()`` is deprecated. - **CodeIgniter:** ``CodeIgniter::$returnResponse`` property is deprecated. No longer used. -- **RedirectException:** ``\CodeIgniter\Router\Exceptions\RedirectException`` is deprecated. Use \CodeIgniter\HTTP\Exceptions\RedirectException instead. +- **RedirectException:** ``\CodeIgniter\Router\Exceptions\RedirectException`` is deprecated. Use ``\CodeIgniter\HTTP\Exceptions\RedirectException`` instead. - **Session:** The property ``$sessionDriverName``, ``$sessionCookieName``, ``$sessionExpiration``, ``$sessionSavePath``, ``$sessionMatchIP``, ``$sessionTimeToUpdate``, and ``$sessionRegenerateDestroy`` in ``Session`` are From 8b36556cfc0022f0452b4e7b2819e0680bbae573 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 28 Jun 2023 07:15:35 +0900 Subject: [PATCH 281/485] docs: add note for RedirectException namespace --- user_guide_src/source/general/errors.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst index d5de5ca1acbd..c2531586311d 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -104,6 +104,10 @@ This provides an exit code of 8. RedirectException ----------------- +.. note:: Since v4.4.0, the namespace of ``RedirectException`` has been changed. + Previously it was ``CodeIgniter\Router\Exceptions\RedirectException``. The + previous class is deprecated. + This exception is a special case allowing for overriding of all other response routing and forcing a redirect to a specific route or URL: From 8c6e8833834195c9856d8a34837aea30eee94c2d Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 28 Jun 2023 09:29:26 +0900 Subject: [PATCH 282/485] feat: add request option `proxy` --- system/HTTP/CURLRequest.php | 6 ++++++ .../HTTP/CURLRequestDoNotShareOptionsTest.php | 14 ++++++++++++++ tests/system/HTTP/CURLRequestTest.php | 14 ++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 377f51e46006..0c8e8c455ffd 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -550,6 +550,12 @@ protected function setCURLOptions(array $curlOptions = [], array $config = []) } } + // Proxy + if (isset($config['proxy'])) { + $curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true; + $curlOptions[CURLOPT_PROXY] = $config['proxy']; + } + // Debug if ($config['debug']) { $curlOptions[CURLOPT_VERBOSE] = 1; diff --git a/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php index 7dcc47f40082..20e5435d7cad 100644 --- a/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php +++ b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php @@ -559,6 +559,20 @@ public function testSSLWithBadKey() ]); } + public function testProxyuOption() + { + $this->request->request('get', 'http://example.com', [ + 'proxy' => 'http://localhost:3128', + ]); + + $options = $this->request->curl_options; + + $this->assertArrayHasKey(CURLOPT_PROXY, $options); + $this->assertSame('http://localhost:3128', $options[CURLOPT_PROXY]); + $this->assertArrayHasKey(CURLOPT_HTTPPROXYTUNNEL, $options); + $this->assertTrue($options[CURLOPT_HTTPPROXYTUNNEL]); + } + public function testDebugOptionTrue() { $this->request->request('get', 'http://example.com', [ diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index 92c43cc58461..6f99779e7c71 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -542,6 +542,20 @@ public function testSSLWithBadKey() ]); } + public function testProxyuOption() + { + $this->request->request('get', 'http://example.com', [ + 'proxy' => 'http://localhost:3128', + ]); + + $options = $this->request->curl_options; + + $this->assertArrayHasKey(CURLOPT_PROXY, $options); + $this->assertSame('http://localhost:3128', $options[CURLOPT_PROXY]); + $this->assertArrayHasKey(CURLOPT_HTTPPROXYTUNNEL, $options); + $this->assertTrue($options[CURLOPT_HTTPPROXYTUNNEL]); + } + public function testDebugOptionTrue() { $this->request->request('get', 'http://example.com', [ From 35d7d2941eaa9b1db1f23c6fd8191f39fc5f0dee Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 28 Jun 2023 09:30:05 +0900 Subject: [PATCH 283/485] docs: update docs --- user_guide_src/source/changelogs/v4.4.0.rst | 3 ++- user_guide_src/source/libraries/curlrequest.rst | 11 +++++++++++ user_guide_src/source/libraries/curlrequest/035.php | 7 +++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/libraries/curlrequest/035.php diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index e340d33cc667..1668a181cef8 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -95,9 +95,10 @@ Libraries - **Validation:** Added ``Validation::getValidated()`` method that gets the actual validated data. See :ref:`validation-getting-validated-data` for details. - **Images:** The option ``$quality`` can now be used to compress WebP images. - - **Uploaded Files:** Added ``UploadedFiles::getClientPath()`` method that returns the value of the `full_path` index of the file if it was uploaded via directory upload. +- **CURLRequest:** Added a request option ``proxy``. See + :ref:`CURLRequest Class `. Helpers and Functions ===================== diff --git a/user_guide_src/source/libraries/curlrequest.rst b/user_guide_src/source/libraries/curlrequest.rst index 18afd51f771c..b2ecbcdf5f0c 100644 --- a/user_guide_src/source/libraries/curlrequest.rst +++ b/user_guide_src/source/libraries/curlrequest.rst @@ -296,6 +296,17 @@ has been disabled. Any files that you want to send must be passed as instances o ``form_params`` for ``application/x-www-form-urlencoded`` requests, and ``multipart`` for ``multipart/form-data`` requests. +.. _curlrequest-request-options-proxy: + +proxy +===== + +.. versionadded:: 4.4.0 + +You can set a proxy by passing an associative array as the ``proxy`` option: + +.. literalinclude:: curlrequest/035.php + query ===== diff --git a/user_guide_src/source/libraries/curlrequest/035.php b/user_guide_src/source/libraries/curlrequest/035.php new file mode 100644 index 000000000000..a729f9f3be22 --- /dev/null +++ b/user_guide_src/source/libraries/curlrequest/035.php @@ -0,0 +1,7 @@ +request( + 'GET', + 'http://example.com', + ['proxy' => 'http://localhost:3128'] +); From afeb419d0670aab135fec15d70a892b86174cc14 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 14:45:44 +0900 Subject: [PATCH 284/485] refactor: remove CSRF config items in Config\App --- app/Config/App.php | 85 ------------------------------------ system/Security/Security.php | 26 ++++------- 2 files changed, 8 insertions(+), 103 deletions(-) diff --git a/app/Config/App.php b/app/Config/App.php index 69b596bcad59..186bfa86bb02 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -158,91 +158,6 @@ class App extends BaseConfig */ public array $proxyIPs = []; - /** - * -------------------------------------------------------------------------- - * CSRF Token Name - * -------------------------------------------------------------------------- - * - * The token name. - * - * @deprecated Use `Config\Security` $tokenName property instead of using this property. - */ - public string $CSRFTokenName = 'csrf_test_name'; - - /** - * -------------------------------------------------------------------------- - * CSRF Header Name - * -------------------------------------------------------------------------- - * - * The header name. - * - * @deprecated Use `Config\Security` $headerName property instead of using this property. - */ - public string $CSRFHeaderName = 'X-CSRF-TOKEN'; - - /** - * -------------------------------------------------------------------------- - * CSRF Cookie Name - * -------------------------------------------------------------------------- - * - * The cookie name. - * - * @deprecated Use `Config\Security` $cookieName property instead of using this property. - */ - public string $CSRFCookieName = 'csrf_cookie_name'; - - /** - * -------------------------------------------------------------------------- - * CSRF Expire - * -------------------------------------------------------------------------- - * - * The number in seconds the token should expire. - * - * @deprecated Use `Config\Security` $expire property instead of using this property. - */ - public int $CSRFExpire = 7200; - - /** - * -------------------------------------------------------------------------- - * CSRF Regenerate - * -------------------------------------------------------------------------- - * - * Regenerate token on every submission? - * - * @deprecated Use `Config\Security` $regenerate property instead of using this property. - */ - public bool $CSRFRegenerate = true; - - /** - * -------------------------------------------------------------------------- - * CSRF Redirect - * -------------------------------------------------------------------------- - * - * Redirect to previous page with error on failure? - * - * @deprecated Use `Config\Security` $redirect property instead of using this property. - */ - public bool $CSRFRedirect = false; - - /** - * -------------------------------------------------------------------------- - * CSRF SameSite - * -------------------------------------------------------------------------- - * - * Setting for CSRF SameSite cookie token. Allowed values are: - * - None - * - Lax - * - Strict - * - '' - * - * Defaults to `Lax` as recommended in this link: - * - * @see https://portswigger.net/web-security/csrf/samesite-cookies - * - * @deprecated `Config\Cookie` $samesite property is used. - */ - public string $CSRFSameSite = 'Lax'; - /** * -------------------------------------------------------------------------- * Content Security Policy diff --git a/system/Security/Security.php b/system/Security/Security.php index d7a5d49c6f09..e54e28a4d7d5 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -174,24 +174,14 @@ public function __construct(App $config) $security = config(SecurityConfig::class); // Store CSRF-related configurations - if ($security instanceof SecurityConfig) { - $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->redirect = $security->redirect ?? $this->redirect; - $this->rawCookieName = $security->cookieName ?? $this->rawCookieName; - $this->expires = $security->expires ?? $this->expires; - $this->tokenRandomize = $security->tokenRandomize ?? $this->tokenRandomize; - } 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; - $this->redirect = $config->CSRFRedirect ?? $this->redirect; - } + $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->redirect = $security->redirect ?? $this->redirect; + $this->rawCookieName = $security->cookieName ?? $this->rawCookieName; + $this->expires = $security->expires ?? $this->expires; + $this->tokenRandomize = $security->tokenRandomize ?? $this->tokenRandomize; if ($this->isCSRFCookie()) { $cookie = config(CookieConfig::class); From a0665df04285b8492acc22df024b03faaa1f7e9a Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 15:05:28 +0900 Subject: [PATCH 285/485] refactor: add property for SecurityConfig and use it --- system/Security/Security.php | 74 +++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/system/Security/Security.php b/system/Security/Security.php index e54e28a4d7d5..593dc5142b6b 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -44,6 +44,8 @@ class Security implements SecurityInterface * Protection Method for Cross Site Request Forgery protection. * * @var string 'cookie' or 'session' + * + * @deprecated 4.4.0 Use $this->config->csrfProtection. */ protected $csrfProtection = self::CSRF_PROTECTION_COOKIE; @@ -51,6 +53,8 @@ class Security implements SecurityInterface * CSRF Token Randomization * * @var bool + * + * @deprecated 4.4.0 Use $this->config->tokenRandomize. */ protected $tokenRandomize = false; @@ -69,6 +73,8 @@ class Security implements SecurityInterface * Token name for Cross Site Request Forgery protection. * * @var string + * + * @deprecated 4.4.0 Use $this->config->tokenName. */ protected $tokenName = 'csrf_token_name'; @@ -78,6 +84,8 @@ class Security implements SecurityInterface * Header name for Cross Site Request Forgery protection. * * @var string + * + * @deprecated 4.4.0 Use $this->config->headerName. */ protected $headerName = 'X-CSRF-TOKEN'; @@ -105,6 +113,8 @@ class Security implements SecurityInterface * Defaults to two hours (in seconds). * * @var int + * + * @deprecated 4.4.0 Use $this->config->expires. */ protected $expires = 7200; @@ -114,6 +124,8 @@ class Security implements SecurityInterface * Regenerate CSRF Token on every request. * * @var bool + * + * @deprecated 4.4.0 Use $this->config->regenerate. */ protected $regenerate = true; @@ -123,6 +135,8 @@ class Security implements SecurityInterface * Redirect to previous page with error on failure. * * @var bool + * + * @deprecated 4.4.0 Use $this->config->redirect. */ protected $redirect = false; @@ -163,6 +177,11 @@ class Security implements SecurityInterface */ private ?string $hashInCookie = null; + /** + * Security Config + */ + protected SecurityConfig $config; + /** * Constructor. * @@ -171,17 +190,10 @@ class Security implements SecurityInterface */ public function __construct(App $config) { - $security = config(SecurityConfig::class); - - // Store CSRF-related configurations - $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->redirect = $security->redirect ?? $this->redirect; - $this->rawCookieName = $security->cookieName ?? $this->rawCookieName; - $this->expires = $security->expires ?? $this->expires; - $this->tokenRandomize = $security->tokenRandomize ?? $this->tokenRandomize; + $security = config(SecurityConfig::class); + $this->config = $security; + + $this->rawCookieName = $security->cookieName; if ($this->isCSRFCookie()) { $cookie = config(CookieConfig::class); @@ -203,7 +215,7 @@ public function __construct(App $config) private function isCSRFCookie(): bool { - return $this->csrfProtection === self::CSRF_PROTECTION_COOKIE; + return $this->config->csrfProtection === self::CSRF_PROTECTION_COOKIE; } private function configureSession(): void @@ -277,7 +289,7 @@ public function verify(RequestInterface $request) $postedToken = $this->getPostedToken($request); try { - $token = ($postedToken !== null && $this->tokenRandomize) + $token = ($postedToken !== null && $this->config->tokenRandomize) ? $this->derandomize($postedToken) : $postedToken; } catch (InvalidArgumentException $e) { $token = null; @@ -290,7 +302,7 @@ public function verify(RequestInterface $request) $this->removeTokenInRequest($request); - if ($this->regenerate) { + if ($this->config->regenerate) { $this->generateHash(); } @@ -308,13 +320,13 @@ private function removeTokenInRequest(RequestInterface $request): void $json = json_decode($request->getBody() ?? ''); - if (isset($_POST[$this->tokenName])) { + if (isset($_POST[$this->config->tokenName])) { // We kill this since we're done and we don't want to pollute the POST array. - unset($_POST[$this->tokenName]); + unset($_POST[$this->config->tokenName]); $request->setGlobal('post', $_POST); - } elseif (isset($json->{$this->tokenName})) { + } elseif (isset($json->{$this->config->tokenName})) { // We kill this since we're done and we don't want to pollute the JSON data. - unset($json->{$this->tokenName}); + unset($json->{$this->config->tokenName}); $request->setBody(json_encode($json)); } } @@ -325,19 +337,19 @@ private function getPostedToken(RequestInterface $request): ?string // Does the token exist in POST, HEADER or optionally php:://input - json data. - if ($tokenValue = $request->getPost($this->tokenName)) { + if ($tokenValue = $request->getPost($this->config->tokenName)) { return $tokenValue; } - if ($request->hasHeader($this->headerName) && ! empty($request->header($this->headerName)->getValue())) { - return $request->header($this->headerName)->getValue(); + if ($request->hasHeader($this->config->headerName) && ! empty($request->header($this->config->headerName)->getValue())) { + return $request->header($this->config->headerName)->getValue(); } $body = (string) $request->getBody(); $json = json_decode($body); if ($body !== '' && ! empty($json) && json_last_error() === JSON_ERROR_NONE) { - return $json->{$this->tokenName} ?? null; + return $json->{$this->config->tokenName} ?? null; } return null; @@ -348,7 +360,7 @@ private function getPostedToken(RequestInterface $request): ?string */ public function getHash(): ?string { - return $this->tokenRandomize ? $this->randomize($this->hash) : $this->hash; + return $this->config->tokenRandomize ? $this->randomize($this->hash) : $this->hash; } /** @@ -397,7 +409,7 @@ protected function derandomize(string $token): string */ public function getTokenName(): string { - return $this->tokenName; + return $this->config->tokenName; } /** @@ -405,7 +417,7 @@ public function getTokenName(): string */ public function getHeaderName(): string { - return $this->headerName; + return $this->config->headerName; } /** @@ -413,7 +425,7 @@ public function getHeaderName(): string */ public function getCookieName(): string { - return $this->cookieName; + return $this->config->cookieName; } /** @@ -433,7 +445,7 @@ public function isExpired(): bool */ public function shouldRedirect(): bool { - return $this->redirect; + return $this->config->redirect; } /** @@ -511,9 +523,9 @@ private function restoreHash(): void if ($this->isHashInCookie()) { $this->hash = $this->hashInCookie; } - } elseif ($this->session->has($this->tokenName)) { + } elseif ($this->session->has($this->config->tokenName)) { // Session based CSRF protection - $this->hash = $this->session->get($this->tokenName); + $this->hash = $this->session->get($this->config->tokenName); } } @@ -552,7 +564,7 @@ private function saveHashInCookie(): void $this->rawCookieName, $this->hash, [ - 'expires' => $this->expires === 0 ? 0 : Time::now()->getTimestamp() + $this->expires, + 'expires' => $this->config->expires === 0 ? 0 : Time::now()->getTimestamp() + $this->config->expires, ] ); @@ -596,6 +608,6 @@ protected function doSendCookie(): void private function saveHashInSession(): void { - $this->session->set($this->tokenName, $this->hash); + $this->session->set($this->config->tokenName, $this->hash); } } From a9554b350215042cfa856c43c6468c0ad41d01ae Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 15:27:57 +0900 Subject: [PATCH 286/485] refactor: [BC] change Security constructor param from Config\App to Config\Security --- system/Config/Services.php | 5 ++- system/Security/Security.php | 8 ++--- tests/system/Config/ServicesTest.php | 5 +-- tests/system/Helpers/SecurityHelperTest.php | 4 +-- .../SecurityCSRFCookieRandomizeTokenTest.php | 16 +++++---- .../SecurityCSRFSessionRandomizeTokenTest.php | 36 ++++++++++--------- .../Security/SecurityCSRFSessionTest.php | 30 ++++++++-------- tests/system/Security/SecurityTest.php | 30 ++++++++-------- 8 files changed, 71 insertions(+), 63 deletions(-) diff --git a/system/Config/Services.php b/system/Config/Services.php index 89a49b4bf468..779afa3ccaf1 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -77,6 +77,7 @@ use Config\Pager as PagerConfig; use Config\Paths; use Config\Routing; +use Config\Security as SecurityConfig; use Config\Services as AppServices; use Config\Session as SessionConfig; use Config\Toolbar as ToolbarConfig; @@ -626,6 +627,8 @@ public static function router(?RouteCollectionInterface $routes = null, ?Request * secure, most notably the CSRF protection tools. * * @return Security + * + * @TODO replace the first parameter type `?App` with `?SecurityConfig` */ public static function security(?App $config = null, bool $getShared = true) { @@ -633,7 +636,7 @@ public static function security(?App $config = null, bool $getShared = true) return static::getSharedInstance('security', $config); } - $config ??= config(App::class); + $config = config(SecurityConfig::class); return new Security($config); } diff --git a/system/Security/Security.php b/system/Security/Security.php index 593dc5142b6b..c67c13ce0c1c 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -18,7 +18,6 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Security\Exceptions\SecurityException; use CodeIgniter\Session\Session; -use Config\App; use Config\Cookie as CookieConfig; use Config\Security as SecurityConfig; use Config\Services; @@ -188,12 +187,11 @@ class Security implements SecurityInterface * Stores our configuration and fires off the init() method to setup * initial state. */ - public function __construct(App $config) + public function __construct(SecurityConfig $config) { - $security = config(SecurityConfig::class); - $this->config = $security; + $this->config = $config; - $this->rawCookieName = $security->cookieName; + $this->rawCookieName = $config->cookieName; if ($this->isCSRFCookie()) { $cookie = config(CookieConfig::class); diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 143591aac10c..b6ea85e6971f 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -43,6 +43,7 @@ use CodeIgniter\View\Parser; use Config\App; use Config\Exceptions; +use Config\Security as SecurityConfig; use RuntimeException; use Tests\Support\Config\Services; @@ -329,7 +330,7 @@ public function testReset() public function testResetSingle() { Services::injectMock('response', new MockResponse(new App())); - Services::injectMock('security', new MockSecurity(new App())); + Services::injectMock('security', new MockSecurity(new SecurityConfig())); $response = service('response'); $security = service('security'); $this->assertInstanceOf(MockResponse::class, $response); @@ -411,7 +412,7 @@ public function testRouter() public function testSecurity() { - Services::injectMock('security', new MockSecurity(new App())); + Services::injectMock('security', new MockSecurity(new SecurityConfig())); $result = Services::security(); $this->assertInstanceOf(Security::class, $result); diff --git a/tests/system/Helpers/SecurityHelperTest.php b/tests/system/Helpers/SecurityHelperTest.php index c7523e3b0081..8c2c0d6efd55 100644 --- a/tests/system/Helpers/SecurityHelperTest.php +++ b/tests/system/Helpers/SecurityHelperTest.php @@ -13,7 +13,7 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockSecurity; -use Config\App; +use Config\Security as SecurityConfig; use Tests\Support\Config\Services; /** @@ -32,7 +32,7 @@ protected function setUp(): void public function testSanitizeFilenameSimpleSuccess() { - Services::injectMock('security', new MockSecurity(new App())); + Services::injectMock('security', new MockSecurity(new SecurityConfig())); $this->assertSame('hello.doc', sanitize_filename('hello.doc')); } diff --git a/tests/system/Security/SecurityCSRFCookieRandomizeTokenTest.php b/tests/system/Security/SecurityCSRFCookieRandomizeTokenTest.php index 93f749ffc604..95ad54d58b04 100644 --- a/tests/system/Security/SecurityCSRFCookieRandomizeTokenTest.php +++ b/tests/system/Security/SecurityCSRFCookieRandomizeTokenTest.php @@ -38,19 +38,21 @@ final class SecurityCSRFCookieRandomizeTokenTest extends CIUnitTestCase */ private string $randomizedToken = '8bc70b67c91494e815c7d2219c1ae0ab005513c290126d34d41bf41c5265e0f1'; + private SecurityConfig $config; + protected function setUp(): void { parent::setUp(); $_COOKIE = []; - $config = new SecurityConfig(); - $config->csrfProtection = Security::CSRF_PROTECTION_COOKIE; - $config->tokenRandomize = true; - Factories::injectMock('config', 'Security', $config); + $this->config = new SecurityConfig(); + $this->config->csrfProtection = Security::CSRF_PROTECTION_COOKIE; + $this->config->tokenRandomize = true; + Factories::injectMock('config', 'Security', $this->config); // Set Cookie value - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity($this->config); $_COOKIE[$security->getCookieName()] = $this->hash; $this->resetServices(); @@ -58,7 +60,7 @@ protected function setUp(): void public function testTokenIsReadFromCookie() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity($this->config); $this->assertSame( $this->randomizedToken, @@ -74,7 +76,7 @@ public function testCSRFVerifySetNewCookie() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); diff --git a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php index 36bd5ea97f1c..6afc1aeefd22 100644 --- a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php +++ b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php @@ -51,6 +51,8 @@ final class SecurityCSRFSessionRandomizeTokenTest extends CIUnitTestCase */ private string $randomizedToken = '8bc70b67c91494e815c7d2219c1ae0ab005513c290126d34d41bf41c5265e0f1'; + private SecurityConfig $config; + protected function setUp(): void { parent::setUp(); @@ -58,10 +60,10 @@ protected function setUp(): void $_SESSION = []; Factories::reset(); - $config = new SecurityConfig(); - $config->csrfProtection = Security::CSRF_PROTECTION_SESSION; - $config->tokenRandomize = true; - Factories::injectMock('config', 'Security', $config); + $this->config = new SecurityConfig(); + $this->config->csrfProtection = Security::CSRF_PROTECTION_SESSION; + $this->config->tokenRandomize = true; + Factories::injectMock('config', 'Security', $this->config); $this->injectSession($this->hash); } @@ -113,7 +115,7 @@ private function injectSession(string $hash): void public function testHashIsReadFromSession() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity($this->config); $this->assertSame( $this->randomizedToken, @@ -131,7 +133,7 @@ public function testCSRFVerifyPostNoToken() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $security->verify($request); } @@ -146,7 +148,7 @@ public function testCSRFVerifyPostThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $security->verify($request); } @@ -161,7 +163,7 @@ public function testCSRFVerifyPostInvalidToken() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $security->verify($request); } @@ -174,7 +176,7 @@ public function testCSRFVerifyPostReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -188,7 +190,7 @@ public function testCSRFVerifyPOSTHeaderThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005b'); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->expectException(SecurityException::class); $this->expectExceptionMessage('The action you requested is not allowed.'); @@ -204,7 +206,7 @@ public function testCSRFVerifyPOSTHeaderReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', $this->randomizedToken); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -218,7 +220,7 @@ public function testCSRFVerifyPUTHeaderThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005b'); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->expectException(SecurityException::class); $this->expectExceptionMessage('The action you requested is not allowed.'); @@ -233,7 +235,7 @@ public function testCSRFVerifyPUTHeaderReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', $this->randomizedToken); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -249,7 +251,7 @@ public function testCSRFVerifyJsonThrowsExceptionOnNoMatch() $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 = new Security($this->config); $security->verify($request); } @@ -261,7 +263,7 @@ public function testCSRFVerifyJsonReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setBody('{"csrf_test_name":"' . $this->randomizedToken . '","foo":"bar"}'); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -280,7 +282,7 @@ public function testRegenerateWithFalseSecurityRegenerateProperty() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity($this->config); $oldHash = $security->getHash(); $security->verify($request); @@ -301,7 +303,7 @@ public function testRegenerateWithTrueSecurityRegenerateProperty() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $oldHash = $security->getHash(); $security->verify($request); diff --git a/tests/system/Security/SecurityCSRFSessionTest.php b/tests/system/Security/SecurityCSRFSessionTest.php index d6f2216e29f6..41ccc638331b 100644 --- a/tests/system/Security/SecurityCSRFSessionTest.php +++ b/tests/system/Security/SecurityCSRFSessionTest.php @@ -45,6 +45,8 @@ final class SecurityCSRFSessionTest extends CIUnitTestCase */ private string $hash = '8b9218a55906f9dcc1dc263dce7f005a'; + private SecurityConfig $config; + protected function setUp(): void { parent::setUp(); @@ -52,9 +54,9 @@ protected function setUp(): void $_SESSION = []; Factories::reset(); - $config = new SecurityConfig(); - $config->csrfProtection = Security::CSRF_PROTECTION_SESSION; - Factories::injectMock('config', 'Security', $config); + $this->config = new SecurityConfig(); + $this->config->csrfProtection = Security::CSRF_PROTECTION_SESSION; + Factories::injectMock('config', 'Security', $this->config); $this->injectSession($this->hash); } @@ -106,7 +108,7 @@ private function injectSession(string $hash): void public function testHashIsReadFromSession() { - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->assertSame($this->hash, $security->getHash()); } @@ -120,7 +122,7 @@ public function testCSRFVerifyPostThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security(new MockAppConfig()); + $security = new Security(new SecurityConfig()); $security->verify($request); } @@ -133,7 +135,7 @@ public function testCSRFVerifyPostReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -147,7 +149,7 @@ public function testCSRFVerifyPOSTHeaderThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005b'); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->expectException(SecurityException::class); $security->verify($request); @@ -161,7 +163,7 @@ public function testCSRFVerifyPOSTHeaderReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -175,7 +177,7 @@ public function testCSRFVerifyPUTHeaderThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005b'); - $security = new Security(new MockAppConfig()); + $security = new Security(new SecurityConfig()); $this->expectException(SecurityException::class); $security->verify($request); @@ -188,7 +190,7 @@ public function testCSRFVerifyPUTHeaderReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -203,7 +205,7 @@ public function testCSRFVerifyJsonThrowsExceptionOnNoMatch() $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 = new Security(new SecurityConfig()); $security->verify($request); } @@ -215,7 +217,7 @@ public function testCSRFVerifyJsonReturnsSelfOnMatch() $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()); + $security = new Security($this->config); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -233,7 +235,7 @@ public function testRegenerateWithFalseSecurityRegenerateProperty() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $oldHash = $security->getHash(); $security->verify($request); @@ -253,7 +255,7 @@ public function testRegenerateWithTrueSecurityRegenerateProperty() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security(new MockAppConfig()); + $security = new Security($this->config); $oldHash = $security->getHash(); $security->verify($request); diff --git a/tests/system/Security/SecurityTest.php b/tests/system/Security/SecurityTest.php index f610add149f0..82bc6b6c27db 100644 --- a/tests/system/Security/SecurityTest.php +++ b/tests/system/Security/SecurityTest.php @@ -43,7 +43,7 @@ protected function setUp(): void public function testBasicConfigIsSaved() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $hash = $security->getHash(); @@ -55,7 +55,7 @@ public function testHashIsReadFromCookie() { $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $this->assertSame( '8b9218a55906f9dcc1dc263dce7f005a', @@ -65,7 +65,7 @@ public function testHashIsReadFromCookie() public function testGetHashSetsCookieWhenGETWithoutCSRFCookie() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $_SERVER['REQUEST_METHOD'] = 'GET'; @@ -80,7 +80,7 @@ public function testGetHashReturnsCSRFCookieWhenGETWithCSRFCookie() $_SERVER['REQUEST_METHOD'] = 'GET'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $security->verify(new Request(new MockAppConfig())); @@ -93,7 +93,7 @@ public function testCSRFVerifyPostThrowsExceptionOnNoMatch() $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -112,7 +112,7 @@ public function testCSRFVerifyPostReturnsSelfOnMatch() $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -131,7 +131,7 @@ public function testCSRFVerifyHeaderThrowsExceptionOnNoMatch() $_SERVER['REQUEST_METHOD'] = 'POST'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -151,7 +151,7 @@ public function testCSRFVerifyHeaderReturnsSelfOnMatch() $_POST['foo'] = 'bar'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -172,7 +172,7 @@ public function testCSRFVerifyJsonThrowsExceptionOnNoMatch() $_SERVER['REQUEST_METHOD'] = 'POST'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -193,7 +193,7 @@ public function testCSRFVerifyJsonReturnsSelfOnMatch() $_SERVER['REQUEST_METHOD'] = 'POST'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -213,7 +213,7 @@ public function testCSRFVerifyJsonReturnsSelfOnMatch() public function testSanitizeFilename() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $filename = './'; @@ -230,7 +230,7 @@ public function testRegenerateWithFalseSecurityRegenerateProperty() $config->regenerate = false; Factories::injectMock('config', 'Security', $config); - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity($config); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -255,7 +255,7 @@ public function testRegenerateWithFalseSecurityRegeneratePropertyManually() $config->regenerate = false; Factories::injectMock('config', 'Security', $config); - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -281,7 +281,7 @@ public function testRegenerateWithTrueSecurityRegenerateProperty() $config->regenerate = true; Factories::injectMock('config', 'Security', $config); - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -298,7 +298,7 @@ public function testRegenerateWithTrueSecurityRegenerateProperty() public function testGetters(): void { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new SecurityConfig()); $this->assertIsString($security->getHash()); $this->assertIsString($security->getTokenName()); From 5e7a1e6e5b54b7964d5d846f18eccb240c05a112 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 15:29:46 +0900 Subject: [PATCH 287/485] docs: add changelog and upgrade note --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + user_guide_src/source/installation/upgrade_440.rst | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index b4e705557979..d8efc4622037 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -135,6 +135,7 @@ Changes - **Images:** The default quality for WebP in ``GDHandler`` has been changed from 80 to 90. - **Config:** The deprecated Cookie items in **app/Config/App.php** has been removed. - **Config:** The deprecated Session items in **app/Config/App.php** has been removed. +- **Config:** The deprecated CSRF items in **app/Config/App.php** has been removed. - **Config:** Routing settings have been moved to **app/Config/Routing.php** config file. See :ref:`Upgrading Guide `. - **DownloadResponse:** When generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index c82723aa129d..3eb8a9728555 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -115,6 +115,16 @@ The Cookie config items in **app/Config/App.php** are no longer used. 2. Remove the properties (from ``$cookiePrefix`` to ``$cookieSameSite``) in **app/Config/App.php**. +app/Config/Security.php +----------------------- + +The CSRF config items in **app/Config/App.php** are no longer used. + +1. Copy **app/Config/Security.php** from the new framework to your **app/Config** + directory, and configure it. +2. Remove the properties (from ``$CSRFTokenName`` to ``$CSRFSameSite``) in + **app/Config/App.php**. + app/Config/Session.php ---------------------- From 7a7e98089a63f42dfb227d3b17e66cecc2077763 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 15:38:16 +0900 Subject: [PATCH 288/485] refactor: remove CSRF properties in MockAppConfig --- system/Test/Mock/MockAppConfig.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/system/Test/Mock/MockAppConfig.php b/system/Test/Mock/MockAppConfig.php index 0d3a22cc3ff6..0f1d5bf59975 100644 --- a/system/Test/Mock/MockAppConfig.php +++ b/system/Test/Mock/MockAppConfig.php @@ -18,14 +18,6 @@ class MockAppConfig extends App public string $baseURL = 'http://example.com/'; public string $uriProtocol = 'REQUEST_URI'; public array $proxyIPs = []; - public string $CSRFTokenName = 'csrf_test_name'; - public string $CSRFHeaderName = 'X-CSRF-TOKEN'; - public string $CSRFCookieName = 'csrf_cookie_name'; - public int $CSRFExpire = 7200; - public bool $CSRFRegenerate = true; - public array $CSRFExcludeURIs = ['http://example.com']; - public bool $CSRFRedirect = false; - public string $CSRFSameSite = 'Lax'; public bool $CSPEnabled = false; public string $defaultLocale = 'en'; public bool $negotiateLocale = false; From 848436f861ae472d717b4b4a14109801d94087b5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 15:49:56 +0900 Subject: [PATCH 289/485] test: extract method to create instance --- .../SecurityCSRFSessionRandomizeTokenTest.php | 27 ++++++++------ .../Security/SecurityCSRFSessionTest.php | 27 ++++++++------ tests/system/Security/SecurityTest.php | 35 +++++++++++-------- 3 files changed, 53 insertions(+), 36 deletions(-) diff --git a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php index 6afc1aeefd22..087f3a7a74df 100644 --- a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php +++ b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php @@ -113,6 +113,11 @@ private function injectSession(string $hash): void Services::injectMock('session', $session); } + private function createSecurity(): Security + { + return new Security($this->config); + } + public function testHashIsReadFromSession() { $security = new MockSecurity($this->config); @@ -133,7 +138,7 @@ public function testCSRFVerifyPostNoToken() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security($this->config); + $security = $this->createSecurity(); $security->verify($request); } @@ -148,7 +153,7 @@ public function testCSRFVerifyPostThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security($this->config); + $security = $this->createSecurity(); $security->verify($request); } @@ -163,7 +168,7 @@ public function testCSRFVerifyPostInvalidToken() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security($this->config); + $security = $this->createSecurity(); $security->verify($request); } @@ -176,7 +181,7 @@ public function testCSRFVerifyPostReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security($this->config); + $security = $this->createSecurity(); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -190,7 +195,7 @@ public function testCSRFVerifyPOSTHeaderThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005b'); - $security = new Security($this->config); + $security = $this->createSecurity(); $this->expectException(SecurityException::class); $this->expectExceptionMessage('The action you requested is not allowed.'); @@ -206,7 +211,7 @@ public function testCSRFVerifyPOSTHeaderReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', $this->randomizedToken); - $security = new Security($this->config); + $security = $this->createSecurity(); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -220,7 +225,7 @@ public function testCSRFVerifyPUTHeaderThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005b'); - $security = new Security($this->config); + $security = $this->createSecurity(); $this->expectException(SecurityException::class); $this->expectExceptionMessage('The action you requested is not allowed.'); @@ -235,7 +240,7 @@ public function testCSRFVerifyPUTHeaderReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', $this->randomizedToken); - $security = new Security($this->config); + $security = $this->createSecurity(); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -251,7 +256,7 @@ public function testCSRFVerifyJsonThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setBody('{"csrf_test_name":"8b9218a55906f9dcc1dc263dce7f005b"}'); - $security = new Security($this->config); + $security = $this->createSecurity(); $security->verify($request); } @@ -263,7 +268,7 @@ public function testCSRFVerifyJsonReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setBody('{"csrf_test_name":"' . $this->randomizedToken . '","foo":"bar"}'); - $security = new Security($this->config); + $security = $this->createSecurity(); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -303,7 +308,7 @@ public function testRegenerateWithTrueSecurityRegenerateProperty() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security($this->config); + $security = $this->createSecurity(); $oldHash = $security->getHash(); $security->verify($request); diff --git a/tests/system/Security/SecurityCSRFSessionTest.php b/tests/system/Security/SecurityCSRFSessionTest.php index 41ccc638331b..e683e6327485 100644 --- a/tests/system/Security/SecurityCSRFSessionTest.php +++ b/tests/system/Security/SecurityCSRFSessionTest.php @@ -106,9 +106,14 @@ private function injectSession(string $hash): void Services::injectMock('session', $session); } + private function createSecurity(): Security + { + return new Security($this->config); + } + public function testHashIsReadFromSession() { - $security = new Security($this->config); + $security = $this->createSecurity(); $this->assertSame($this->hash, $security->getHash()); } @@ -122,7 +127,7 @@ public function testCSRFVerifyPostThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security(new SecurityConfig()); + $security = $this->createSecurity(); $security->verify($request); } @@ -135,7 +140,7 @@ public function testCSRFVerifyPostReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security($this->config); + $security = $this->createSecurity(); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -149,7 +154,7 @@ public function testCSRFVerifyPOSTHeaderThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005b'); - $security = new Security($this->config); + $security = $this->createSecurity(); $this->expectException(SecurityException::class); $security->verify($request); @@ -163,7 +168,7 @@ public function testCSRFVerifyPOSTHeaderReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); - $security = new Security($this->config); + $security = $this->createSecurity(); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -177,7 +182,7 @@ public function testCSRFVerifyPUTHeaderThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005b'); - $security = new Security(new SecurityConfig()); + $security = $this->createSecurity(); $this->expectException(SecurityException::class); $security->verify($request); @@ -190,7 +195,7 @@ public function testCSRFVerifyPUTHeaderReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); - $security = new Security($this->config); + $security = $this->createSecurity(); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -205,7 +210,7 @@ public function testCSRFVerifyJsonThrowsExceptionOnNoMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setBody('{"csrf_test_name":"8b9218a55906f9dcc1dc263dce7f005b"}'); - $security = new Security(new SecurityConfig()); + $security = $this->createSecurity(); $security->verify($request); } @@ -217,7 +222,7 @@ public function testCSRFVerifyJsonReturnsSelfOnMatch() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setBody('{"csrf_test_name":"8b9218a55906f9dcc1dc263dce7f005a","foo":"bar"}'); - $security = new Security($this->config); + $security = $this->createSecurity(); $this->assertInstanceOf(Security::class, $security->verify($request)); $this->assertLogged('info', 'CSRF token verified.'); @@ -235,7 +240,7 @@ public function testRegenerateWithFalseSecurityRegenerateProperty() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security($this->config); + $security = $this->createSecurity(); $oldHash = $security->getHash(); $security->verify($request); @@ -255,7 +260,7 @@ public function testRegenerateWithTrueSecurityRegenerateProperty() $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $security = new Security($this->config); + $security = $this->createSecurity(); $oldHash = $security->getHash(); $security->verify($request); diff --git a/tests/system/Security/SecurityTest.php b/tests/system/Security/SecurityTest.php index 82bc6b6c27db..e47e112b72bc 100644 --- a/tests/system/Security/SecurityTest.php +++ b/tests/system/Security/SecurityTest.php @@ -41,9 +41,16 @@ protected function setUp(): void $this->resetServices(); } + private function createMockSecurity(?SecurityConfig $config = null): MockSecurity + { + $config ??= new SecurityConfig(); + + return new MockSecurity($config); + } + public function testBasicConfigIsSaved() { - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $hash = $security->getHash(); @@ -55,7 +62,7 @@ public function testHashIsReadFromCookie() { $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $this->assertSame( '8b9218a55906f9dcc1dc263dce7f005a', @@ -65,7 +72,7 @@ public function testHashIsReadFromCookie() public function testGetHashSetsCookieWhenGETWithoutCSRFCookie() { - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $_SERVER['REQUEST_METHOD'] = 'GET'; @@ -80,7 +87,7 @@ public function testGetHashReturnsCSRFCookieWhenGETWithCSRFCookie() $_SERVER['REQUEST_METHOD'] = 'GET'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $security->verify(new Request(new MockAppConfig())); @@ -93,7 +100,7 @@ public function testCSRFVerifyPostThrowsExceptionOnNoMatch() $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -112,7 +119,7 @@ public function testCSRFVerifyPostReturnsSelfOnMatch() $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -131,7 +138,7 @@ public function testCSRFVerifyHeaderThrowsExceptionOnNoMatch() $_SERVER['REQUEST_METHOD'] = 'POST'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -151,7 +158,7 @@ public function testCSRFVerifyHeaderReturnsSelfOnMatch() $_POST['foo'] = 'bar'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -172,7 +179,7 @@ public function testCSRFVerifyJsonThrowsExceptionOnNoMatch() $_SERVER['REQUEST_METHOD'] = 'POST'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -193,7 +200,7 @@ public function testCSRFVerifyJsonReturnsSelfOnMatch() $_SERVER['REQUEST_METHOD'] = 'POST'; $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -213,7 +220,7 @@ public function testCSRFVerifyJsonReturnsSelfOnMatch() public function testSanitizeFilename() { - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $filename = './'; @@ -255,7 +262,7 @@ public function testRegenerateWithFalseSecurityRegeneratePropertyManually() $config->regenerate = false; Factories::injectMock('config', 'Security', $config); - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity($config); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -281,7 +288,7 @@ public function testRegenerateWithTrueSecurityRegenerateProperty() $config->regenerate = true; Factories::injectMock('config', 'Security', $config); - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity($config); $request = new IncomingRequest( new MockAppConfig(), new URI('http://badurl.com'), @@ -298,7 +305,7 @@ public function testRegenerateWithTrueSecurityRegenerateProperty() public function testGetters(): void { - $security = new MockSecurity(new SecurityConfig()); + $security = $this->createMockSecurity(); $this->assertIsString($security->getHash()); $this->assertIsString($security->getTokenName()); From 241ac4c6ff6251a94d9c91ec538402a88218d6fd Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 15:53:17 +0900 Subject: [PATCH 290/485] test: update MockSecurity constructor param --- tests/system/CommonFunctionsTest.php | 3 ++- tests/system/CommonSingleServiceTest.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 884b2819f912..6246286abc23 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -33,6 +33,7 @@ use Config\Logger; use Config\Modules; use Config\Routing; +use Config\Security as SecurityConfig; use Config\Services; use Config\Session as SessionConfig; use Kint; @@ -336,7 +337,7 @@ public function testAppTimezone() public function testCSRFToken() { - Services::injectMock('security', new MockSecurity(new App())); + Services::injectMock('security', new MockSecurity(new SecurityConfig())); $this->assertSame('csrf_test_name', csrf_token()); } diff --git a/tests/system/CommonSingleServiceTest.php b/tests/system/CommonSingleServiceTest.php index 58854f27df68..676aa529bb48 100644 --- a/tests/system/CommonSingleServiceTest.php +++ b/tests/system/CommonSingleServiceTest.php @@ -15,7 +15,7 @@ use CodeIgniter\Config\Services; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockSecurity; -use Config\App; +use Config\Security as SecurityConfig; use ReflectionClass; use ReflectionMethod; @@ -31,7 +31,7 @@ final class CommonSingleServiceTest extends CIUnitTestCase */ public function testSingleServiceWithNoParamsSupplied(string $service): void { - Services::injectMock('security', new MockSecurity(new App())); + Services::injectMock('security', new MockSecurity(new SecurityConfig())); $service1 = single_service($service); $service2 = single_service($service); From 812b172e371fa5f71df27fda89e765d86ff8dc11 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 16:27:40 +0900 Subject: [PATCH 291/485] test: remove out-of-dated test case $CSRFExpire was removed. --- tests/system/CommonFunctionsTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 6246286abc23..5792cdc59efa 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -497,7 +497,6 @@ public function testReallyWritable() public function testSlashItem() { $this->assertSame('en/', slash_item('defaultLocale')); // en - $this->assertSame('7200/', slash_item('CSRFExpire')); // int 7200 $this->assertSame('', slash_item('negotiateLocale')); // false } From eccbae455c5c9bef00bf91adddeb93075926f118 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 16:06:46 +0900 Subject: [PATCH 292/485] docs: add Deprecations in changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index d8efc4622037..fe4d428998ff 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -162,6 +162,10 @@ Deprecations ``$sessionExpiration``, ``$sessionSavePath``, ``$sessionMatchIP``, ``$sessionTimeToUpdate``, and ``$sessionRegenerateDestroy`` in ``Session`` are deprecated, and no longer used. Use ``$config`` instead. +- **Security:** The property ``$csrfProtection``, ``$tokenRandomize``, + ``$tokenName``, ``$headerName``, ``$expires``, ``$regenerate``, and + ``$redirect`` in ``Security`` are deprecated, and no longer used. Use + ``$config`` instead. Bugs Fixed ********** From 2868f5cb1dabc00976abb552ca656c52927b9996 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Jun 2023 17:29:52 +0900 Subject: [PATCH 293/485] docs: add method signature change --- user_guide_src/source/changelogs/v4.4.0.rst | 4 ++++ user_guide_src/source/installation/upgrade_440.rst | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index fe4d428998ff..638cf2efc266 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -40,6 +40,8 @@ Interface Changes - **Validation:** Added the ``getValidated()`` method in ``ValidationInterface``. +.. _v440-method-signature-changes: + Method Signature Changes ======================== @@ -52,6 +54,8 @@ Method Signature Changes - **Session:** The first parameter of ``__construct()`` in ``BaseHandler``, ``DatabaseHandler``, ``FileHandler``, ``MemcachedHandler``, and ``RedisHandler`` has been changed from ``Config\App`` to ``Config\Session``. +- **Security:** The first parameter of ``Security::__construct()`` has been + changed from ``Config\App`` to ``Config\Security``. Enhancements ************ diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 3eb8a9728555..0080a767bdc0 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -75,6 +75,13 @@ performance. If you extend ``RouteCollection`` and use the ``$routes``, update your code to match the new array structure. +Method Signature Changes +======================== + +Some method signature changes have been made. Classes that extend them should +update their APIs to reflect the changes. See :ref:`v440-method-signature-changes` +for details. + Mandatory File Changes ********************** From 2fb429d1c525a701266f0376643917977fa08032 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Sat, 24 Jun 2023 05:19:55 +0800 Subject: [PATCH 294/485] rework: RedirectException implements ResponsableInterface --- system/CodeIgniter.php | 12 ++++++------ system/HTTP/Exceptions/RedirectException.php | 15 ++++++++++++++- .../HTTP/Exceptions/ResponsableInterface.php | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 system/HTTP/Exceptions/ResponsableInterface.php diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 66a05aa99df7..c85082ad67fa 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -19,6 +19,7 @@ use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\DownloadResponse; use CodeIgniter\HTTP\Exceptions\RedirectException; +use CodeIgniter\HTTP\Exceptions\ResponsableInterface; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\Request; @@ -343,14 +344,13 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon try { $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse); - } catch (RedirectException|DeprecatedRedirectException $e) { + } catch (ResponsableInterface|DeprecatedRedirectException $e) { $this->outputBufferingEnd(); - $logger = Services::logger(); - $logger->info('REDIRECTED ROUTE at ' . $e->getMessage()); + if ($e instanceof DeprecatedRedirectException) { + $e = new RedirectException($e->getMessage(), $e->getCode(), $e); + } - // If the route is a 'redirect' route, it throws - // the exception with the $to as the message - $this->response->redirect(base_url($e->getMessage()), 'auto', $e->getCode()); + $this->response = $e->getResponse(); } catch (PageNotFoundException $e) { $this->response = $this->display404errors($e); } catch (Throwable $e) { diff --git a/system/HTTP/Exceptions/RedirectException.php b/system/HTTP/Exceptions/RedirectException.php index 5cda9fb00cd0..37b7e3fbcf14 100644 --- a/system/HTTP/Exceptions/RedirectException.php +++ b/system/HTTP/Exceptions/RedirectException.php @@ -12,12 +12,14 @@ namespace CodeIgniter\HTTP\Exceptions; use CodeIgniter\Exceptions\HTTPExceptionInterface; +use CodeIgniter\HTTP\ResponseInterface; +use Config\Services; use Exception; /** * RedirectException */ -class RedirectException extends Exception implements HTTPExceptionInterface +class RedirectException extends Exception implements ResponsableInterface, HTTPExceptionInterface { /** * HTTP status code for redirects @@ -25,4 +27,15 @@ class RedirectException extends Exception implements HTTPExceptionInterface * @var int */ protected $code = 302; + + public function getResponse(): ResponseInterface + { + $logger = Services::logger(); + $response = Services::response(); + $logger->info('REDIRECTED ROUTE at ' . $this->getMessage()); + + // If the route is a 'redirect' route, it throws + // the exception with the $to as the message + return $response->redirect(base_url($this->getMessage()), 'auto', $this->getCode()); + } } diff --git a/system/HTTP/Exceptions/ResponsableInterface.php b/system/HTTP/Exceptions/ResponsableInterface.php new file mode 100644 index 000000000000..e765f96784ac --- /dev/null +++ b/system/HTTP/Exceptions/ResponsableInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP\Exceptions; + +use CodeIgniter\HTTP\ResponseInterface; + +interface ResponsableInterface +{ + public function getResponse(): ResponseInterface; +} From 33b7963dfe7776f1169924e74ef7697234d47309 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Sat, 24 Jun 2023 06:54:14 +0800 Subject: [PATCH 295/485] rework: RedirectException constructor. force_https --- system/CodeIgniter.php | 4 +-- system/Common.php | 36 +++++++++++--------- system/HTTP/Exceptions/RedirectException.php | 29 ++++++++++++++++ tests/system/CodeIgniterTest.php | 4 +-- tests/system/CommonFunctionsTest.php | 19 +++++++++-- tests/system/ControllerTest.php | 12 ++++--- 6 files changed, 77 insertions(+), 27 deletions(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index c85082ad67fa..f40f3393eeae 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -338,8 +338,6 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon $this->getRequestObject(); $this->getResponseObject(); - $this->forceSecureAccess(); - $this->spoofRequestMethod(); try { @@ -419,6 +417,8 @@ public function disableFilters(): void */ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cacheConfig, bool $returnResponse = false) { + $this->forceSecureAccess(); + if ($this->request instanceof IncomingRequest && strtolower($this->request->getMethod()) === 'cli') { return $this->response->setStatusCode(405)->setBody('Method Not Allowed'); } diff --git a/system/Common.php b/system/Common.php index 8d366973f14d..1eb561445ce6 100644 --- a/system/Common.php +++ b/system/Common.php @@ -21,6 +21,7 @@ use CodeIgniter\Files\Exceptions\FileNotFoundException; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; @@ -476,22 +477,24 @@ function esc($data, string $context = 'html', ?string $encoding = null) * @param ResponseInterface $response * * @throws HTTPException + * @throws RedirectException */ - function force_https(int $duration = 31_536_000, ?RequestInterface $request = null, ?ResponseInterface $response = null) - { - if ($request === null) { - $request = Services::request(null, true); - } + function force_https( + int $duration = 31_536_000, + ?RequestInterface $request = null, + ?ResponseInterface $response = null + ) { + $request ??= Services::request(); if (! $request instanceof IncomingRequest) { return; } - if ($response === null) { - $response = Services::response(null, true); - } + $response ??= Services::response(); - if ((ENVIRONMENT !== 'testing' && (is_cli() || $request->isSecure())) || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'test')) { + if ((ENVIRONMENT !== 'testing' && (is_cli() || $request->isSecure())) + || $request->getServer('HTTPS') === 'test' + ) { return; // @codeCoverageIgnore } @@ -520,13 +523,14 @@ function force_https(int $duration = 31_536_000, ?RequestInterface $request = nu ); // Set an HSTS header - $response->setHeader('Strict-Transport-Security', 'max-age=' . $duration); - $response->redirect($uri); - $response->sendHeaders(); - - if (ENVIRONMENT !== 'testing') { - exit(); // @codeCoverageIgnore - } + $response->setHeader('Strict-Transport-Security', 'max-age=' . $duration) + ->redirect($uri) + ->setStatusCode(307) + ->setBody('') + ->getCookieStore() + ->clear(); + + throw new RedirectException($response); } } diff --git a/system/HTTP/Exceptions/RedirectException.php b/system/HTTP/Exceptions/RedirectException.php index 37b7e3fbcf14..16ebc11ccd14 100644 --- a/system/HTTP/Exceptions/RedirectException.php +++ b/system/HTTP/Exceptions/RedirectException.php @@ -15,6 +15,8 @@ use CodeIgniter\HTTP\ResponseInterface; use Config\Services; use Exception; +use InvalidArgumentException; +use Throwable; /** * RedirectException @@ -28,8 +30,35 @@ class RedirectException extends Exception implements ResponsableInterface, HTTPE */ protected $code = 302; + protected ?ResponseInterface $response = null; + + /** + * @param ResponseInterface|string $message + */ + public function __construct($message = '', int $code = 0, ?Throwable $previous = null) + { + if (! is_string($message) && ! $message instanceof ResponseInterface) { + throw new InvalidArgumentException( + 'RedirectException::__construct() first argument must be a string or ResponseInterface', + 0, + $this + ); + } + + if ($message instanceof ResponseInterface) { + $this->response = $message; + $message = ''; + } + + parent::__construct($message, $code, $previous); + } + public function getResponse(): ResponseInterface { + if (null !== $this->response) { + return $this->response; + } + $logger = Services::logger(); $response = Services::response(); $logger->info('REDIRECTED ROUTE at ' . $this->getMessage()); diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 8f45881ba944..b2430a26a7c8 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -441,9 +441,7 @@ public function testRunForceSecure() $response = $this->getPrivateProperty($codeigniter, 'response'); $this->assertNull($response->header('Location')); - ob_start(); - $codeigniter->run(); - ob_get_clean(); + $response = $codeigniter->run(null, true); $this->assertSame('https://example.com/', $response->header('Location')->getValue()); } diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 884b2819f912..5a1add9ab2fd 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -14,6 +14,7 @@ use CodeIgniter\Config\BaseService; use CodeIgniter\Config\Factories; use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\Response; @@ -35,6 +36,7 @@ use Config\Routing; use Config\Services; use Config\Session as SessionConfig; +use Exception; use Kint; use RuntimeException; use stdClass; @@ -599,10 +601,23 @@ public function testViewNotSaveData() public function testForceHttpsNullRequestAndResponse() { $this->assertNull(Services::response()->header('Location')); + Services::response()->setCookie('force', 'cookie'); + Services::response()->setHeader('Force', 'header'); + Services::response()->setBody('default body'); + + try { + force_https(); + } catch (Exception $e) { + $this->assertInstanceOf(RedirectException::class, $e); + $this->assertSame('https://example.com/', $e->getResponse()->header('Location')->getValue()); + $this->assertFalse($e->getResponse()->hasCookie('force')); + $this->assertSame('header', $e->getResponse()->getHeaderLine('Force')); + $this->assertSame('', $e->getResponse()->getBody()); + $this->assertSame(307, $e->getResponse()->getStatusCode()); + } + $this->expectException(RedirectException::class); force_https(); - - $this->assertSame('https://example.com/', Services::response()->header('Location')->getValue()); } /** diff --git a/tests/system/ControllerTest.php b/tests/system/ControllerTest.php index 00c03a5abc0e..ed42410db4b9 100644 --- a/tests/system/ControllerTest.php +++ b/tests/system/ControllerTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter; use CodeIgniter\Config\Factories; +use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\Response; @@ -75,10 +76,13 @@ public function testConstructorHTTPS() $original = $_SERVER; $_SERVER = ['HTTPS' => 'on']; // make sure we can instantiate one - $this->controller = new class () extends Controller { - protected $forceHTTPS = 1; - }; - $this->controller->initController($this->request, $this->response, $this->logger); + try { + $this->controller = new class () extends Controller { + protected $forceHTTPS = 1; + }; + $this->controller->initController($this->request, $this->response, $this->logger); + } catch (RedirectException $e) { + } $this->assertInstanceOf(Controller::class, $this->controller); $_SERVER = $original; // restore so code coverage doesn't break From 7d5b9f8cfa847c4575bef6bdcaa5791d18f4af2f Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Sun, 25 Jun 2023 08:26:10 +0800 Subject: [PATCH 296/485] rework: RedirectException. UG and new tests --- system/CodeIgniter.php | 2 +- system/HTTP/Exceptions/RedirectException.php | 12 ++++ .../{Exceptions => }/ResponsableInterface.php | 4 +- tests/system/HTTP/RedirectExceptionTest.php | 61 +++++++++++++++++++ user_guide_src/source/changelogs/v4.4.0.rst | 3 + user_guide_src/source/general/errors.rst | 5 ++ user_guide_src/source/general/errors/018.php | 8 +++ 7 files changed, 91 insertions(+), 4 deletions(-) rename system/HTTP/{Exceptions => }/ResponsableInterface.php (81%) create mode 100644 tests/system/HTTP/RedirectExceptionTest.php create mode 100644 user_guide_src/source/general/errors/018.php diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index f40f3393eeae..06b0c1f5c500 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -19,10 +19,10 @@ use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\DownloadResponse; use CodeIgniter\HTTP\Exceptions\RedirectException; -use CodeIgniter\HTTP\Exceptions\ResponsableInterface; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\Request; +use CodeIgniter\HTTP\ResponsableInterface; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\URI; use CodeIgniter\Router\Exceptions\RedirectException as DeprecatedRedirectException; diff --git a/system/HTTP/Exceptions/RedirectException.php b/system/HTTP/Exceptions/RedirectException.php index 16ebc11ccd14..25c2e3d8df47 100644 --- a/system/HTTP/Exceptions/RedirectException.php +++ b/system/HTTP/Exceptions/RedirectException.php @@ -12,10 +12,12 @@ namespace CodeIgniter\HTTP\Exceptions; use CodeIgniter\Exceptions\HTTPExceptionInterface; +use CodeIgniter\HTTP\ResponsableInterface; use CodeIgniter\HTTP\ResponseInterface; use Config\Services; use Exception; use InvalidArgumentException; +use LogicException; use Throwable; /** @@ -48,6 +50,16 @@ public function __construct($message = '', int $code = 0, ?Throwable $previous = if ($message instanceof ResponseInterface) { $this->response = $message; $message = ''; + + if ($this->response->getHeaderLine('Location') === '' && $this->response->getHeaderLine('Refresh') === '') { + throw new LogicException( + 'The Response object passed to RedirectException does not contain a redirect address.' + ); + } + + if ($this->response->getStatusCode() < 301 || $this->response->getStatusCode() > 308) { + $this->response->setStatusCode($this->code); + } } parent::__construct($message, $code, $previous); diff --git a/system/HTTP/Exceptions/ResponsableInterface.php b/system/HTTP/ResponsableInterface.php similarity index 81% rename from system/HTTP/Exceptions/ResponsableInterface.php rename to system/HTTP/ResponsableInterface.php index e765f96784ac..0cca5356f1f7 100644 --- a/system/HTTP/Exceptions/ResponsableInterface.php +++ b/system/HTTP/ResponsableInterface.php @@ -9,9 +9,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\HTTP\Exceptions; - -use CodeIgniter\HTTP\ResponseInterface; +namespace CodeIgniter\HTTP; interface ResponsableInterface { diff --git a/tests/system/HTTP/RedirectExceptionTest.php b/tests/system/HTTP/RedirectExceptionTest.php new file mode 100644 index 000000000000..b33570d4332c --- /dev/null +++ b/tests/system/HTTP/RedirectExceptionTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\HTTP\Exceptions\RedirectException; +use Config\Services; +use LogicException; +use PHPUnit\Framework\TestCase; + +/** + * @internal + */ +final class RedirectExceptionTest extends TestCase +{ + protected function setUp(): void + { + Services::reset(); + } + + public function testResponse(): void + { + $response = Services::response() + ->redirect('redirect') + ->setCookie('cookie', 'value') + ->setHeader('Redirect-Header', 'value'); + $exception = new RedirectException($response); + + $this->assertSame('redirect', $exception->getResponse()->getHeaderLine('location')); + $this->assertSame(302, $exception->getResponse()->getStatusCode()); + $this->assertSame('value', $exception->getResponse()->getHeaderLine('Redirect-Header')); + $this->assertSame('value', $exception->getResponse()->getCookie('cookie')->getValue()); + } + + public function testResponseWithoutLocation(): void + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'The Response object passed to RedirectException does not contain a redirect address.' + ); + + new RedirectException(Services::response()); + } + + public function testResponseWithoutStatusCode(): void + { + $response = Services::response()->setHeader('Location', 'location'); + $exception = new RedirectException($response); + + $this->assertSame('location', $exception->getResponse()->getHeaderLine('location')); + $this->assertSame(302, $exception->getResponse()->getStatusCode()); + } +} diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index b4e705557979..9e2be53da2bb 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -123,6 +123,9 @@ Others - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. - **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with headings. See :ref:`table-sync-rows-with-headings` for details. - **Error Handling:** Now you can use :ref:`custom-exception-handlers`. +- **RedirectException:** can also take an object that implements ResponseInterface as its first argument. +- **RedirectException:** implements ResponsableInterface +- **force_https:** no longer terminates the application, but throws a RedirectException. Message Changes *************** diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst index 8ba2ccd82d34..bdc9c939258e 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -118,6 +118,11 @@ redirect code to use instead of the default (``302``, "temporary redirect"): .. literalinclude:: errors/011.php +Also, an object of a class that implements ResponseInterface can be used as the first argument. +This solution is suitable for cases where you need to add additional headers or cookies in the response. + +.. literalinclude:: errors/018.php + .. _error-specify-http-status-code: Specify HTTP Status Code in Your Exception diff --git a/user_guide_src/source/general/errors/018.php b/user_guide_src/source/general/errors/018.php new file mode 100644 index 000000000000..3cba09951d8b --- /dev/null +++ b/user_guide_src/source/general/errors/018.php @@ -0,0 +1,8 @@ +redirect('https://example.com/path') + ->setHeader('Some', 'header') + ->setCookie('and', 'cookie'); + +throw new \CodeIgniter\HTTP\Exceptions\RedirectException($response); From 8a770a55b637d9e124fbc9e8838bf4cb2c008b96 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Sun, 25 Jun 2023 08:32:47 +0800 Subject: [PATCH 297/485] rework: RedirectException. test group --- tests/system/HTTP/RedirectExceptionTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/system/HTTP/RedirectExceptionTest.php b/tests/system/HTTP/RedirectExceptionTest.php index b33570d4332c..79a530aba08c 100644 --- a/tests/system/HTTP/RedirectExceptionTest.php +++ b/tests/system/HTTP/RedirectExceptionTest.php @@ -18,6 +18,8 @@ /** * @internal + * + * @group Others */ final class RedirectExceptionTest extends TestCase { From 3e1fa646fdf49e57d0addda3d0c1216dfa88b1e5 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Tue, 27 Jun 2023 23:26:22 +0800 Subject: [PATCH 298/485] rework: RedirectException. tests, docs and changes --- system/HTTP/Exceptions/RedirectException.php | 19 ++++--- tests/system/HTTP/RedirectExceptionTest.php | 58 +++++++++++++++----- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/system/HTTP/Exceptions/RedirectException.php b/system/HTTP/Exceptions/RedirectException.php index 25c2e3d8df47..605f40eaba33 100644 --- a/system/HTTP/Exceptions/RedirectException.php +++ b/system/HTTP/Exceptions/RedirectException.php @@ -35,7 +35,8 @@ class RedirectException extends Exception implements ResponsableInterface, HTTPE protected ?ResponseInterface $response = null; /** - * @param ResponseInterface|string $message + * @param ResponseInterface|string $message Response object or a string containing a relative URI. + * @param int $code HTTP status code to redirect if $message is a string. */ public function __construct($message = '', int $code = 0, ?Throwable $previous = null) { @@ -67,16 +68,16 @@ public function __construct($message = '', int $code = 0, ?Throwable $previous = public function getResponse(): ResponseInterface { - if (null !== $this->response) { - return $this->response; + if (null === $this->response) { + $this->response = Services::response() + ->redirect(base_url($this->getMessage()), 'auto', $this->getCode()); } - $logger = Services::logger(); - $response = Services::response(); - $logger->info('REDIRECTED ROUTE at ' . $this->getMessage()); + Services::logger()->info( + 'REDIRECTED ROUTE at ' + . ($this->response->getHeaderLine('Location') ?: substr($this->response->getHeaderLine('Refresh'), 6)) + ); - // If the route is a 'redirect' route, it throws - // the exception with the $to as the message - return $response->redirect(base_url($this->getMessage()), 'auto', $this->getCode()); + return $this->response; } } diff --git a/tests/system/HTTP/RedirectExceptionTest.php b/tests/system/HTTP/RedirectExceptionTest.php index 79a530aba08c..acf91a392582 100644 --- a/tests/system/HTTP/RedirectExceptionTest.php +++ b/tests/system/HTTP/RedirectExceptionTest.php @@ -12,9 +12,12 @@ namespace CodeIgniter\HTTP; use CodeIgniter\HTTP\Exceptions\RedirectException; +use CodeIgniter\Log\Logger; +use CodeIgniter\Test\Mock\MockLogger as LoggerConfig; use Config\Services; use LogicException; use PHPUnit\Framework\TestCase; +use Tests\Support\Log\Handlers\TestHandler; /** * @internal @@ -26,20 +29,22 @@ final class RedirectExceptionTest extends TestCase protected function setUp(): void { Services::reset(); + Services::injectMock('logger', new Logger(new LoggerConfig())); } public function testResponse(): void { - $response = Services::response() - ->redirect('redirect') - ->setCookie('cookie', 'value') - ->setHeader('Redirect-Header', 'value'); - $exception = new RedirectException($response); - - $this->assertSame('redirect', $exception->getResponse()->getHeaderLine('location')); - $this->assertSame(302, $exception->getResponse()->getStatusCode()); - $this->assertSame('value', $exception->getResponse()->getHeaderLine('Redirect-Header')); - $this->assertSame('value', $exception->getResponse()->getCookie('cookie')->getValue()); + $response = (new RedirectException( + Services::response() + ->redirect('redirect') + ->setCookie('cookie', 'value') + ->setHeader('Redirect-Header', 'value') + ))->getResponse(); + + $this->assertSame('redirect', $response->getHeaderLine('location')); + $this->assertSame(302, $response->getStatusCode()); + $this->assertSame('value', $response->getHeaderLine('Redirect-Header')); + $this->assertSame('value', $response->getCookie('cookie')->getValue()); } public function testResponseWithoutLocation(): void @@ -54,10 +59,35 @@ public function testResponseWithoutLocation(): void public function testResponseWithoutStatusCode(): void { - $response = Services::response()->setHeader('Location', 'location'); - $exception = new RedirectException($response); + $response = (new RedirectException(Services::response()->setHeader('Location', 'location')))->getResponse(); + + $this->assertSame('location', $response->getHeaderLine('location')); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testLoggingLocationHeader(): void + { + $uri = 'http://location'; + $expected = 'INFO - ' . date('Y-m-d') . ' --> REDIRECTED ROUTE at ' . $uri; + $response = (new RedirectException(Services::response()->redirect($uri)))->getResponse(); + + $logs = TestHandler::getLogs(); + + $this->assertSame($uri, $response->getHeaderLine('Location')); + $this->assertSame('', $response->getHeaderLine('Refresh')); + $this->assertSame($expected, $logs[0]); + } + + public function testLoggingRefreshHeader(): void + { + $uri = 'http://location'; + $expected = 'INFO - ' . date('Y-m-d') . ' --> REDIRECTED ROUTE at ' . $uri; + $response = (new RedirectException(Services::response()->redirect($uri, 'refresh')))->getResponse(); + + $logs = TestHandler::getLogs(); - $this->assertSame('location', $exception->getResponse()->getHeaderLine('location')); - $this->assertSame(302, $exception->getResponse()->getStatusCode()); + $this->assertSame($uri, substr($response->getHeaderLine('Refresh'), 6)); + $this->assertSame('', $response->getHeaderLine('Location')); + $this->assertSame($expected, $logs[0]); } } From 17583a126540537ca91baab021877aaa036230ff Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Wed, 28 Jun 2023 07:42:56 +0800 Subject: [PATCH 299/485] rework: RedirectException. changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 9e2be53da2bb..2a405e665138 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -104,6 +104,7 @@ Helpers and Functions - **Array:** Added :php:func:`array_group_by()` helper function to group data values together. Supports dot-notation syntax. +- **Common:** :php:func:`force_https()` no longer terminates the application, but throws a ``RedirectException``. Others ====== @@ -124,8 +125,7 @@ Others - **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with headings. See :ref:`table-sync-rows-with-headings` for details. - **Error Handling:** Now you can use :ref:`custom-exception-handlers`. - **RedirectException:** can also take an object that implements ResponseInterface as its first argument. -- **RedirectException:** implements ResponsableInterface -- **force_https:** no longer terminates the application, but throws a RedirectException. +- **RedirectException:** implements ResponsableInterface. Message Changes *************** From 9de07170c38ffc559c0abf14259a829d5c36e265 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Wed, 28 Jun 2023 10:21:29 +0800 Subject: [PATCH 300/485] Update user_guide_src/source/general/errors.rst Version of the new functionality. Co-authored-by: kenjis --- user_guide_src/source/general/errors.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst index bdc9c939258e..d2b48716cf9e 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -118,7 +118,7 @@ redirect code to use instead of the default (``302``, "temporary redirect"): .. literalinclude:: errors/011.php -Also, an object of a class that implements ResponseInterface can be used as the first argument. +Also, since v4.4.0 an object of a class that implements ResponseInterface can be used as the first argument. This solution is suitable for cases where you need to add additional headers or cookies in the response. .. literalinclude:: errors/018.php From abb921a7b7a2c171bbfef2703b7985334bbf4a8b Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Thu, 29 Jun 2023 08:46:38 +0800 Subject: [PATCH 301/485] rebase --- user_guide_src/source/changelogs/v4.4.0.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 2a405e665138..20495ad2874e 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -149,6 +149,10 @@ Changes this restriction has been removed. - **RouteCollection:** The array structure of the protected property ``$routes`` has been modified for performance. +- **HSTS:** Now :php:func:`force_https()` or + ``Config\App::$forceGlobalSecureRequests = true`` sets the HTTP status code 307, + which allows the HTTP request method to be preserved after the redirect. + In previous versions, it was 302. Deprecations ************ From 6c3b6c4f1851854155dee228505c532c810f5e8f Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 29 Jun 2023 12:02:46 +0900 Subject: [PATCH 302/485] config: change the default value of $shareOptions to false --- app/Config/CURLRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Config/CURLRequest.php b/app/Config/CURLRequest.php index 6c3ed74aa987..5a3d4e9311b2 100644 --- a/app/Config/CURLRequest.php +++ b/app/Config/CURLRequest.php @@ -16,5 +16,5 @@ class CURLRequest extends BaseConfig * If true, all the options won't be reset between requests. * It may cause an error request with unnecessary headers. */ - public bool $shareOptions = true; + public bool $shareOptions = false; } From 15677c30c6ad898a13342d4b106f3abe40296211 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 29 Jun 2023 12:05:16 +0900 Subject: [PATCH 303/485] docs: update user guide --- user_guide_src/source/installation/upgrade_440.rst | 2 ++ user_guide_src/source/libraries/curlrequest.rst | 14 ++++++++++---- .../source/libraries/curlrequest/001.php | 3 +-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index c82723aa129d..39aa16e07716 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -153,6 +153,8 @@ and it is recommended that you merge the updated versions with your application: Config ------ +- app/Config/CURLRequest.php + - The default value of :ref:`$shareOptions ` has been change to ``false``. - app/Config/Exceptions.php - Added the new method ``handler()`` that define custom Exception Handlers. See :ref:`custom-exception-handlers`. diff --git a/user_guide_src/source/libraries/curlrequest.rst b/user_guide_src/source/libraries/curlrequest.rst index 18afd51f771c..d0741ee366b8 100644 --- a/user_guide_src/source/libraries/curlrequest.rst +++ b/user_guide_src/source/libraries/curlrequest.rst @@ -23,17 +23,23 @@ to change very little to move over to use Guzzle. Config for CURLRequest ********************** +.. _curlrequest-sharing-options: + 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 and body. +.. note:: Since v4.4.0, the default value has been changed to ``false``. This + setting exists only for backward compatibility. New users do not need to + change the setting. -You can change the behavior by editing the following config parameter value in **app/Config/CURLRequest.php** to ``false``: +If you want to share all the options between requests, set ``$shareOptions`` to +``true`` in **app/Config/CURLRequest.php**: .. literalinclude:: curlrequest/001.php +If you send more than one request with an instance of the class, this behavior +may cause an error request with unnecessary headers and body. + .. note:: Before v4.2.0, the request body is not reset even if ``$shareOptions`` is false due to a bug. ******************* diff --git a/user_guide_src/source/libraries/curlrequest/001.php b/user_guide_src/source/libraries/curlrequest/001.php index e89e0692462f..303366336dbb 100644 --- a/user_guide_src/source/libraries/curlrequest/001.php +++ b/user_guide_src/source/libraries/curlrequest/001.php @@ -6,7 +6,6 @@ class CURLRequest extends BaseConfig { - public $shareOptions = false; - // ... + public bool $shareOptions = true; } From 713db9e2c98e4ec87fb54f093d287bc894722805 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 29 Jun 2023 18:30:44 +0900 Subject: [PATCH 304/485] fix: change Services::security() param type --- system/Config/Services.php | 6 ++---- user_guide_src/source/changelogs/v4.4.0.rst | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/system/Config/Services.php b/system/Config/Services.php index 779afa3ccaf1..5e0c4e70b96e 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -627,16 +627,14 @@ public static function router(?RouteCollectionInterface $routes = null, ?Request * secure, most notably the CSRF protection tools. * * @return Security - * - * @TODO replace the first parameter type `?App` with `?SecurityConfig` */ - public static function security(?App $config = null, bool $getShared = true) + public static function security(?SecurityConfig $config = null, bool $getShared = true) { if ($getShared) { return static::getSharedInstance('security', $config); } - $config = config(SecurityConfig::class); + $config ??= config(SecurityConfig::class); return new Security($config); } diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 638cf2efc266..403e2ecb2829 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -45,6 +45,8 @@ Interface Changes Method Signature Changes ======================== +- **Services:** The first parameter of ``Services::security()`` has been + changed from ``Config\App`` to ``Config\Security``. - **Routing:** The third parameter ``Routing $routing`` has been added to ``RouteCollection::__construct()``. - **Validation:** The method signature of ``Validation::check()`` has been changed. From 24e9cad7b7956dd7e2a3e5570eb6c65131d8bfa7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 29 Jun 2023 19:37:15 +0900 Subject: [PATCH 305/485] docs: add instruction in upgrade note --- user_guide_src/source/changelogs/v4.4.0.rst | 17 +++++++++++++---- .../source/installation/upgrade_440.rst | 18 +++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 403e2ecb2829..fd9fd2740a1c 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -45,12 +45,13 @@ Interface Changes Method Signature Changes ======================== +.. _v440-parameter-type-changes: + +Parameter Type Changes +---------------------- + - **Services:** The first parameter of ``Services::security()`` has been changed from ``Config\App`` to ``Config\Security``. -- **Routing:** The third parameter ``Routing $routing`` has been added to - ``RouteCollection::__construct()``. -- **Validation:** The method signature of ``Validation::check()`` has been changed. - The ``string`` typehint on the ``$rule`` parameter was removed. - **Session:** The second parameter of ``Session::__construct()`` has been changed from ``Config\App`` to ``Config\Session``. - **Session:** The first parameter of ``__construct()`` in ``BaseHandler``, @@ -58,6 +59,14 @@ Method Signature Changes has been changed from ``Config\App`` to ``Config\Session``. - **Security:** The first parameter of ``Security::__construct()`` has been changed from ``Config\App`` to ``Config\Security``. +- **Validation:** The method signature of ``Validation::check()`` has been changed. + The ``string`` typehint on the ``$rule`` parameter was removed. + +Added Parameters +---------------- + +- **Routing:** The third parameter ``Routing $routing`` has been added to + ``RouteCollection::__construct()``. Enhancements ************ diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 0080a767bdc0..f2018c9684dd 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -66,6 +66,17 @@ Interface Changes Some interface changes have been made. Classes that implement them should update their APIs to reflect the changes. See :ref:`v440-interface-changes` for details. +Method Signature Changes +======================== + +Some method signature changes have been made. Classes that extend them should +update their APIs to reflect the changes. See :ref:`v440-method-signature-changes` +for details. + +Also, the parameter types of some constructors and ``Services::security()`` have changed. +If you call them with the parameters, change the parameter values. +See :ref:`v440-parameter-type-changes` for details. + RouteCollection::$routes ======================== @@ -75,13 +86,6 @@ performance. If you extend ``RouteCollection`` and use the ``$routes``, update your code to match the new array structure. -Method Signature Changes -======================== - -Some method signature changes have been made. Classes that extend them should -update their APIs to reflect the changes. See :ref:`v440-method-signature-changes` -for details. - Mandatory File Changes ********************** From f6435c3e8d1c8ad413d253b6125519e3761170d2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 30 Jun 2023 17:20:29 +0900 Subject: [PATCH 306/485] refactor: extract PageCache class --- system/Cache/PageCache.php | 130 ++++++++++++++ system/CodeIgniter.php | 36 ++-- system/Config/BaseService.php | 2 + system/Config/Services.php | 18 ++ tests/system/Cache/PageCacheTest.php | 249 +++++++++++++++++++++++++++ 5 files changed, 415 insertions(+), 20 deletions(-) create mode 100644 system/Cache/PageCache.php create mode 100644 tests/system/Cache/PageCacheTest.php diff --git a/system/Cache/PageCache.php b/system/Cache/PageCache.php new file mode 100644 index 000000000000..ad52f02433bc --- /dev/null +++ b/system/Cache/PageCache.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Cache; + +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\ResponseInterface; +use Config\Cache as CacheConfig; +use Exception; + +/** + * Web Page Caching + */ +class PageCache +{ + /** + * Whether to take the URL query string into consideration when generating + * output cache files. Valid options are: + * + * false = Disabled + * true = Enabled, take all query parameters into account. + * Please be aware that this may result in numerous cache + * files generated for the same page over and over again. + * array('q') = Enabled, but only take into account the specified list + * of query parameters. + * + * @var bool|string[] + */ + protected $cacheQueryString = false; + + protected CacheInterface $cache; + + public function __construct(CacheConfig $config, CacheInterface $cache) + { + $this->cacheQueryString = $config->cacheQueryString; + $this->cache = $cache; + } + + /** + * Generates the cache key to use from the current request. + * + * @param CLIRequest|IncomingRequest $request + * + * @internal for testing purposes only + */ + public function generateCacheKey($request): string + { + if ($request instanceof CLIRequest) { + return md5($request->getPath()); + } + + $uri = clone $request->getUri(); + + $query = $this->cacheQueryString + ? $uri->getQuery(is_array($this->cacheQueryString) ? ['only' => $this->cacheQueryString] : []) + : ''; + + return md5($uri->setFragment('')->setQuery($query)); + } + + /** + * Caches the full response from the current request. + * + * @param CLIRequest|IncomingRequest $request + * + * @params int $ttl time to live in seconds. + */ + public function cachePage($request, ResponseInterface $response, int $ttl): bool + { + $headers = []; + + foreach ($response->headers() as $header) { + $headers[$header->getName()] = $header->getValueLine(); + } + + return $this->cache->save( + $this->generateCacheKey($request), + serialize(['headers' => $headers, 'output' => $response->getBody()]), + $ttl + ); + } + + /** + * Gets the cached response for the request. + * + * @param CLIRequest|IncomingRequest $request + */ + public function getCachedResponse($request, ResponseInterface $response): ?ResponseInterface + { + if ($cachedResponse = $this->cache->get($this->generateCacheKey($request))) { + $cachedResponse = unserialize($cachedResponse); + + if ( + ! is_array($cachedResponse) + || ! isset($cachedResponse['output']) + || ! isset($cachedResponse['headers']) + ) { + throw new Exception('Error unserializing page cache'); + } + + $headers = $cachedResponse['headers']; + $output = $cachedResponse['output']; + + // Clear all default headers + foreach (array_keys($response->headers()) as $key) { + $response->removeHeader($key); + } + + // Set cached headers + foreach ($headers as $name => $value) { + $response->setHeader($name, $value); + } + + $response->setBody($output); + + return $response; + } + + return null; + } +} diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 06b0c1f5c500..b8625b281978 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -12,6 +12,7 @@ namespace CodeIgniter; use Closure; +use CodeIgniter\Cache\PageCache; use CodeIgniter\Debug\Timer; use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\FrameworkException; @@ -175,6 +176,11 @@ class CodeIgniter */ protected int $bufferLevel; + /** + * Web Page Caching + */ + protected PageCache $pageCache; + /** * Constructor. */ @@ -182,6 +188,8 @@ public function __construct(App $config) { $this->startTime = microtime(true); $this->config = $config; + + $this->pageCache = Services::pagecache(); } /** @@ -518,7 +526,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache // so that we can have live speed updates along the way. // Must be run after filters to preserve the Response headers. if (static::$cacheTTL > 0) { - $this->cachePage($cacheConfig); + $this->pageCache->cachePage($this->request, $this->response, static::$cacheTTL); } // Update the performance metrics @@ -674,27 +682,11 @@ protected function forceSecureAccess($duration = 31_536_000) */ public function displayCache(Cache $config) { - if ($cachedResponse = cache()->get($this->generateCacheName($config))) { - $cachedResponse = unserialize($cachedResponse); - if (! is_array($cachedResponse) || ! isset($cachedResponse['output']) || ! isset($cachedResponse['headers'])) { - throw new Exception('Error unserializing page cache'); - } - - $headers = $cachedResponse['headers']; - $output = $cachedResponse['output']; - - // Clear all default headers - foreach (array_keys($this->response->headers()) as $key) { - $this->response->removeHeader($key); - } - - // Set cached headers - foreach ($headers as $name => $value) { - $this->response->setHeader($name, $value); - } + if ($cachedResponse = $this->pageCache->getCachedResponse($this->request, $this->response)) { + $this->response = $cachedResponse; $this->totalTime = $this->benchmark->getElapsedTime('total_execution'); - $output = $this->displayPerformanceMetrics($output); + $output = $this->displayPerformanceMetrics($cachedResponse->getBody()); $this->response->setBody($output); return $this->response; @@ -716,6 +708,8 @@ public static function cache(int $time) * full-page caching for very high performance. * * @return bool + * + * @deprecated 4.4.0 No longer used. */ public function cachePage(Cache $config) { @@ -741,6 +735,8 @@ public function getPerformanceStats(): array /** * Generates the cache name to use for our full-page caching. + * + * @deprecated 4.4.0 No longer used. */ protected function generateCacheName(Cache $config): string { diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index d517691b0c1f..f98ec9192856 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -14,6 +14,7 @@ use CodeIgniter\Autoloader\Autoloader; use CodeIgniter\Autoloader\FileLocator; use CodeIgniter\Cache\CacheInterface; +use CodeIgniter\Cache\PageCache; use CodeIgniter\CLI\Commands; use CodeIgniter\CodeIgniter; use CodeIgniter\Database\ConnectionInterface; @@ -111,6 +112,7 @@ * @method static Logger logger($getShared = true) * @method static MigrationRunner migrations(Migrations $config = null, ConnectionInterface $db = null, $getShared = true) * @method static Negotiate negotiator(RequestInterface $request = null, $getShared = true) + * @method static PageCache pagecache(?Cache $config = null, ?CacheInterface $cache = null, bool $getShared = true) * @method static Pager pager(ConfigPager $config = null, RendererInterface $view = null, $getShared = true) * @method static Parser parser($viewPath = null, ConfigView $config = null, $getShared = true) * @method static RedirectResponse redirectresponse(App $config = null, $getShared = true) diff --git a/system/Config/Services.php b/system/Config/Services.php index 5e0c4e70b96e..14c9b122cc36 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -13,6 +13,7 @@ use CodeIgniter\Cache\CacheFactory; use CodeIgniter\Cache\CacheInterface; +use CodeIgniter\Cache\PageCache; use CodeIgniter\CLI\Commands; use CodeIgniter\CodeIgniter; use CodeIgniter\Database\ConnectionInterface; @@ -438,6 +439,23 @@ public static function negotiator(?RequestInterface $request = null, bool $getSh return new Negotiate($request); } + /** + * Return the PageCache. + * + * @return PageCache + */ + public static function pagecache(?Cache $config = null, ?CacheInterface $cache = null, bool $getShared = true) + { + if ($getShared) { + return static::getSharedInstance('pagecache', $config, $cache); + } + + $config ??= config(Cache::class); + $cache ??= AppServices::cache(); + + return new PageCache($config, $cache); + } + /** * Return the appropriate pagination handler. * diff --git a/tests/system/Cache/PageCacheTest.php b/tests/system/Cache/PageCacheTest.php new file mode 100644 index 000000000000..d95c9f10f91f --- /dev/null +++ b/tests/system/Cache/PageCacheTest.php @@ -0,0 +1,249 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Cache; + +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\Response; +use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\HTTP\URI; +use CodeIgniter\HTTP\UserAgent; +use CodeIgniter\Test\CIUnitTestCase; +use Config\App as AppConfig; +use Config\Cache as CacheConfig; +use ErrorException; +use Exception; + +/** + * @backupGlobals enabled + * + * @internal + * + * @group Others + */ +final class PageCacheTest extends CIUnitTestCase +{ + private AppConfig $appConfig; + + protected function setUp(): void + { + parent::setUp(); + + $this->appConfig = new AppConfig(); + } + + private function createIncomingRequest( + string $uri = '', + array $query = [], + ?AppConfig $appConfig = null + ): IncomingRequest { + $_POST = $_GET = $_SERVER = $_REQUEST = $_ENV = $_COOKIE = $_SESSION = []; + + $_SERVER['REQUEST_URI'] = '/' . $uri . ($query ? '?' . http_build_query($query) : ''); + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $appConfig ??= $this->appConfig; + + $siteUri = new URI($appConfig->baseURL . $uri); + if ($query !== []) { + $_GET = $_REQUEST = $query; + $siteUri->setQueryArray($query); + } + + return new IncomingRequest( + $appConfig, + $siteUri, + null, + new UserAgent() + ); + } + + /** + * @phpstan-param list $params + */ + private function createCLIRequest(array $params = [], ?AppConfig $appConfig = null): CLIRequest + { + $_POST = $_GET = $_SERVER = $_REQUEST = $_ENV = $_COOKIE = $_SESSION = []; + + $_SERVER['argv'] = ['public/index.php', ...$params]; + $_SERVER['SCRIPT_NAME'] = 'public/index.php'; + + $appConfig ??= $this->appConfig; + + return new CLIRequest($appConfig); + } + + private function createPageCache(?CacheConfig $cacheConfig = null): pageCache + { + $cache = mock(CacheFactory::class); + + $cacheConfig ??= new CacheConfig(); + + return new PageCache($cacheConfig, $cache); + } + + public function testCachePageIncomingRequest() + { + $pageCache = $this->createPageCache(); + + $request = $this->createIncomingRequest('foo/bar'); + + $response = new Response($this->appConfig); + $response->setHeader('ETag', 'abcd1234'); + $response->setBody('The response body.'); + + $return = $pageCache->cachePage($request, $response, 300); + + $this->assertTrue($return); + + // Check cache with a request with the same URI path. + $request = $this->createIncomingRequest('foo/bar'); + $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + + $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); + $this->assertSame('The response body.', $cachedResponse->getBody()); + $this->assertSame('abcd1234', $cachedResponse->getHeaderLine('ETag')); + + // Check cache with a request with the same URI path and different query string. + $request = $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); + $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + + $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); + $this->assertSame('The response body.', $cachedResponse->getBody()); + $this->assertSame('abcd1234', $cachedResponse->getHeaderLine('ETag')); + + // Check cache with another request with the different URI path. + $request = $this->createIncomingRequest('another'); + + $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + + $this->assertNull($cachedResponse); + } + + public function testCachePageIncomingRequestWithCacheQueryString() + { + $cacheConfig = new CacheConfig(); + $cacheConfig->cacheQueryString = true; + $pageCache = $this->createPageCache($cacheConfig); + + $request = $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); + + $response = new Response($this->appConfig); + $response->setHeader('ETag', 'abcd1234'); + $response->setBody('The response body.'); + + $return = $pageCache->cachePage($request, $response, 300); + + $this->assertTrue($return); + + // Check cache with a request with the same URI path and same query string. + $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); + $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + + $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); + $this->assertSame('The response body.', $cachedResponse->getBody()); + $this->assertSame('abcd1234', $cachedResponse->getHeaderLine('ETag')); + + // Check cache with a request with the same URI path and different query string. + $request = $this->createIncomingRequest('foo/bar', ['xfoo' => 'bar', 'bar' => 'baz']); + $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + + $this->assertNull($cachedResponse); + + // Check cache with another request with the different URI path. + $request = $this->createIncomingRequest('another'); + + $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + + $this->assertNull($cachedResponse); + } + + public function testCachePageCLIRequest() + { + $pageCache = $this->createPageCache(); + + $request = $this->createCLIRequest(['foo', 'bar']); + + $response = new Response($this->appConfig); + $response->setBody('The response body.'); + + $return = $pageCache->cachePage($request, $response, 300); + + $this->assertTrue($return); + + // Check cache with a request with the same params. + $request = $this->createCLIRequest(['foo', 'bar']); + $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + + $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); + $this->assertSame('The response body.', $cachedResponse->getBody()); + + // Check cache with another request with the different params. + $request = $this->createCLIRequest(['baz']); + + $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + + $this->assertNull($cachedResponse); + } + + public function testUnserializeError() + { + $this->expectException(ErrorException::class); + $this->expectExceptionMessage('unserialize(): Error at offset 0 of 12 bytes'); + + $cache = mock(CacheFactory::class); + $cacheConfig = new CacheConfig(); + $pageCache = new PageCache($cacheConfig, $cache); + + $request = $this->createIncomingRequest('foo/bar'); + + $response = new Response($this->appConfig); + $response->setHeader('ETag', 'abcd1234'); + $response->setBody('The response body.'); + + $pageCache->cachePage($request, $response, 300); + + $cacheKey = $pageCache->generateCacheKey($request); + + // Save invalid data. + $cache->save($cacheKey, 'Invalid data'); + + // Check cache with a request with the same URI path. + $pageCache->getCachedResponse($request, new Response($this->appConfig)); + } + + public function testInvalidCacheError() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Error unserializing page cache'); + + $cache = mock(CacheFactory::class); + $cacheConfig = new CacheConfig(); + $pageCache = new PageCache($cacheConfig, $cache); + + $request = $this->createIncomingRequest('foo/bar'); + + $response = new Response($this->appConfig); + $response->setHeader('ETag', 'abcd1234'); + $response->setBody('The response body.'); + + $pageCache->cachePage($request, $response, 300); + + $cacheKey = $pageCache->generateCacheKey($request); + + // Save invalid data. + $cache->save($cacheKey, serialize(['a' => '1'])); + + // Check cache with a request with the same URI path. + $pageCache->getCachedResponse($request, new Response($this->appConfig)); + } +} From 28f4a88166c7a6d455a9af1c38b65823a7a7bcbb Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 30 Jun 2023 20:04:57 +0900 Subject: [PATCH 307/485] fix: [BC] fix wrong types for Requests Fixes the following errors: ------ ------------------------------------------------------------------ Line system/CodeIgniter.php ------ ------------------------------------------------------------------ 529 Parameter #1 $request of method CodeIgniter\Cache\PageCache::cachePage() expects CodeIgniter\HTTP\CLIRequest|CodeIgniter\HTTP\IncomingRequest, CodeIgniter\HTTP\Request|null given. 685 Parameter #1 $request of method CodeIgniter\Cache\PageCache::getCachedResponse() expects CodeIgniter\HTTP\CLIRequest|CodeIgniter\HTTP\IncomingRequest, CodeIgniter\HTTP\Request|null given. ------ ------------------------------------------------------------------ --- phpstan-baseline.neon.dist | 10 ---------- system/CodeIgniter.php | 8 +++++--- system/Test/FeatureTestCase.php | 16 +++++++++++----- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/phpstan-baseline.neon.dist b/phpstan-baseline.neon.dist index 2d60b2b599d2..e1ecece1f5f0 100644 --- a/phpstan-baseline.neon.dist +++ b/phpstan-baseline.neon.dist @@ -25,16 +25,6 @@ parameters: count: 1 path: system/Cache/Handlers/RedisHandler.php - - - message: "#^Call to an undefined method CodeIgniter\\\\HTTP\\\\Request\\:\\:getPost\\(\\)\\.$#" - count: 1 - path: system/CodeIgniter.php - - - - message: "#^Call to an undefined method CodeIgniter\\\\HTTP\\\\Request\\:\\:setLocale\\(\\)\\.$#" - count: 1 - path: system/CodeIgniter.php - - message: "#^Property CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:\\$db \\(CodeIgniter\\\\Database\\\\BaseConnection\\) in empty\\(\\) is not falsy\\.$#" count: 1 diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index b8625b281978..1d4ccd12fed0 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -85,7 +85,7 @@ class CodeIgniter /** * Current request. * - * @var CLIRequest|IncomingRequest|Request|null + * @var CLIRequest|IncomingRequest|null */ protected $request; @@ -471,7 +471,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache return $possibleResponse; } - if ($possibleResponse instanceof Request) { + if ($possibleResponse instanceof IncomingRequest || $possibleResponse instanceof CLIRequest) { $this->request = $possibleResponse; } } @@ -611,9 +611,11 @@ protected function startBenchmark() * Sets a Request object to be used for this request. * Used when running certain tests. * + * @param CLIRequest|IncomingRequest $request + * * @return $this */ - public function setRequest(Request $request) + public function setRequest($request) { $this->request = $request; diff --git a/system/Test/FeatureTestCase.php b/system/Test/FeatureTestCase.php index 7163d3e85717..371e302c59ae 100644 --- a/system/Test/FeatureTestCase.php +++ b/system/Test/FeatureTestCase.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Test; use CodeIgniter\Events\Events; +use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Request; @@ -328,11 +329,13 @@ protected function setupHeaders(IncomingRequest $request) * * Always populate the GET vars based on the URI. * - * @return Request + * @param CLIRequest|IncomingRequest $request + * + * @return CLIRequest|IncomingRequest * * @throws ReflectionException */ - protected function populateGlobals(string $method, Request $request, ?array $params = null) + protected function populateGlobals(string $method, $request, ?array $params = null) { // $params should set the query vars if present, // otherwise set it from the URL. @@ -357,10 +360,13 @@ protected function populateGlobals(string $method, Request $request, ?array $par * This allows the body to be formatted in a way that the controller is going to * expect as in the case of testing a JSON or XML API. * - * @param array|null $params The parameters to be formatted and put in the body. If this is empty, it will get the - * what has been loaded into the request global of the request class. + * @param CLIRequest|IncomingRequest $request + * @param array|null $params The parameters to be formatted and put in the body. If this is empty, it will get the + * what has been loaded into the request global of the request class. + * + * @return CLIRequest|IncomingRequest */ - protected function setRequestBody(Request $request, ?array $params = null): Request + protected function setRequestBody($request, ?array $params = null) { if (isset($this->requestBody) && $this->requestBody !== '') { $request->setBody($this->requestBody); From 397194166a413518e94a325b6aa32f691a0bb294 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 30 Jun 2023 20:09:09 +0900 Subject: [PATCH 308/485] chore: add exemptions for CodeIgniter\Cache\PageCache --- deptrac.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deptrac.yaml b/deptrac.yaml index bf39d4f54734..96cfc8fe522a 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -222,6 +222,10 @@ parameters: - Cache skip_violations: # Individual class exemptions + CodeIgniter\Cache\PageCache: + - CodeIgniter\HTTP\CLIRequest + - CodeIgniter\HTTP\IncomingRequest + - CodeIgniter\HTTP\ResponseInterface CodeIgniter\Entity\Cast\URICast: - CodeIgniter\HTTP\URI CodeIgniter\Log\Handlers\ChromeLoggerHandler: From b2afd719fde71229afa1e2082baa5ca7e6dbcceb Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 3 Jul 2023 11:31:07 +0900 Subject: [PATCH 309/485] refactor: rename PageCache to ResponseCache --- .../Cache/{PageCache.php => ResponseCache.php} | 2 +- system/CodeIgniter.php | 6 +++--- system/Config/BaseService.php | 4 ++-- system/Config/Services.php | 12 ++++++------ .../{PageCacheTest.php => ResponseCacheTest.php} | 16 ++++++++-------- 5 files changed, 20 insertions(+), 20 deletions(-) rename system/Cache/{PageCache.php => ResponseCache.php} (99%) rename tests/system/Cache/{PageCacheTest.php => ResponseCacheTest.php} (94%) diff --git a/system/Cache/PageCache.php b/system/Cache/ResponseCache.php similarity index 99% rename from system/Cache/PageCache.php rename to system/Cache/ResponseCache.php index ad52f02433bc..a04b369b348c 100644 --- a/system/Cache/PageCache.php +++ b/system/Cache/ResponseCache.php @@ -20,7 +20,7 @@ /** * Web Page Caching */ -class PageCache +class ResponseCache { /** * Whether to take the URL query string into consideration when generating diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 1d4ccd12fed0..5ce7ac69b2c4 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -12,7 +12,7 @@ namespace CodeIgniter; use Closure; -use CodeIgniter\Cache\PageCache; +use CodeIgniter\Cache\ResponseCache; use CodeIgniter\Debug\Timer; use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\FrameworkException; @@ -179,7 +179,7 @@ class CodeIgniter /** * Web Page Caching */ - protected PageCache $pageCache; + protected ResponseCache $pageCache; /** * Constructor. @@ -189,7 +189,7 @@ public function __construct(App $config) $this->startTime = microtime(true); $this->config = $config; - $this->pageCache = Services::pagecache(); + $this->pageCache = Services::responsecache(); } /** diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index f98ec9192856..133805a6a585 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -14,7 +14,7 @@ use CodeIgniter\Autoloader\Autoloader; use CodeIgniter\Autoloader\FileLocator; use CodeIgniter\Cache\CacheInterface; -use CodeIgniter\Cache\PageCache; +use CodeIgniter\Cache\ResponseCache; use CodeIgniter\CLI\Commands; use CodeIgniter\CodeIgniter; use CodeIgniter\Database\ConnectionInterface; @@ -112,13 +112,13 @@ * @method static Logger logger($getShared = true) * @method static MigrationRunner migrations(Migrations $config = null, ConnectionInterface $db = null, $getShared = true) * @method static Negotiate negotiator(RequestInterface $request = null, $getShared = true) - * @method static PageCache pagecache(?Cache $config = null, ?CacheInterface $cache = null, bool $getShared = true) * @method static Pager pager(ConfigPager $config = null, RendererInterface $view = null, $getShared = true) * @method static Parser parser($viewPath = null, ConfigView $config = null, $getShared = true) * @method static RedirectResponse redirectresponse(App $config = null, $getShared = true) * @method static View renderer($viewPath = null, ConfigView $config = null, $getShared = true) * @method static IncomingRequest|CLIRequest request(App $config = null, $getShared = true) * @method static ResponseInterface response(App $config = null, $getShared = true) + * @method static ResponseCache responsecache(?Cache $config = null, ?CacheInterface $cache = null, bool $getShared = true) * @method static Router router(RouteCollectionInterface $routes = null, Request $request = null, $getShared = true) * @method static RouteCollection routes($getShared = true) * @method static Security security(App $config = null, $getShared = true) diff --git a/system/Config/Services.php b/system/Config/Services.php index 14c9b122cc36..c8ded8034257 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -13,7 +13,7 @@ use CodeIgniter\Cache\CacheFactory; use CodeIgniter\Cache\CacheInterface; -use CodeIgniter\Cache\PageCache; +use CodeIgniter\Cache\ResponseCache; use CodeIgniter\CLI\Commands; use CodeIgniter\CodeIgniter; use CodeIgniter\Database\ConnectionInterface; @@ -440,20 +440,20 @@ public static function negotiator(?RequestInterface $request = null, bool $getSh } /** - * Return the PageCache. + * Return the ResponseCache. * - * @return PageCache + * @return ResponseCache */ - public static function pagecache(?Cache $config = null, ?CacheInterface $cache = null, bool $getShared = true) + public static function responsecache(?Cache $config = null, ?CacheInterface $cache = null, bool $getShared = true) { if ($getShared) { - return static::getSharedInstance('pagecache', $config, $cache); + return static::getSharedInstance('responsecache', $config, $cache); } $config ??= config(Cache::class); $cache ??= AppServices::cache(); - return new PageCache($config, $cache); + return new ResponseCache($config, $cache); } /** diff --git a/tests/system/Cache/PageCacheTest.php b/tests/system/Cache/ResponseCacheTest.php similarity index 94% rename from tests/system/Cache/PageCacheTest.php rename to tests/system/Cache/ResponseCacheTest.php index d95c9f10f91f..75038e2753fa 100644 --- a/tests/system/Cache/PageCacheTest.php +++ b/tests/system/Cache/ResponseCacheTest.php @@ -30,7 +30,7 @@ * * @group Others */ -final class PageCacheTest extends CIUnitTestCase +final class ResponseCacheTest extends CIUnitTestCase { private AppConfig $appConfig; @@ -82,18 +82,18 @@ private function createCLIRequest(array $params = [], ?AppConfig $appConfig = nu return new CLIRequest($appConfig); } - private function createPageCache(?CacheConfig $cacheConfig = null): pageCache + private function createResponseCache(?CacheConfig $cacheConfig = null): ResponseCache { $cache = mock(CacheFactory::class); $cacheConfig ??= new CacheConfig(); - return new PageCache($cacheConfig, $cache); + return new ResponseCache($cacheConfig, $cache); } public function testCachePageIncomingRequest() { - $pageCache = $this->createPageCache(); + $pageCache = $this->createResponseCache(); $request = $this->createIncomingRequest('foo/bar'); @@ -133,7 +133,7 @@ public function testCachePageIncomingRequestWithCacheQueryString() { $cacheConfig = new CacheConfig(); $cacheConfig->cacheQueryString = true; - $pageCache = $this->createPageCache($cacheConfig); + $pageCache = $this->createResponseCache($cacheConfig); $request = $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); @@ -169,7 +169,7 @@ public function testCachePageIncomingRequestWithCacheQueryString() public function testCachePageCLIRequest() { - $pageCache = $this->createPageCache(); + $pageCache = $this->createResponseCache(); $request = $this->createCLIRequest(['foo', 'bar']); @@ -202,7 +202,7 @@ public function testUnserializeError() $cache = mock(CacheFactory::class); $cacheConfig = new CacheConfig(); - $pageCache = new PageCache($cacheConfig, $cache); + $pageCache = new ResponseCache($cacheConfig, $cache); $request = $this->createIncomingRequest('foo/bar'); @@ -228,7 +228,7 @@ public function testInvalidCacheError() $cache = mock(CacheFactory::class); $cacheConfig = new CacheConfig(); - $pageCache = new PageCache($cacheConfig, $cache); + $pageCache = new ResponseCache($cacheConfig, $cache); $request = $this->createIncomingRequest('foo/bar'); From d0b986802f5d8c82a9c70c5d4421ebdd3e762ac5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 3 Jul 2023 11:33:05 +0900 Subject: [PATCH 310/485] refactor: rename cachePage() to make() --- system/Cache/ResponseCache.php | 2 +- system/CodeIgniter.php | 2 +- tests/system/Cache/ResponseCacheTest.php | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/system/Cache/ResponseCache.php b/system/Cache/ResponseCache.php index a04b369b348c..20e871cc2f3c 100644 --- a/system/Cache/ResponseCache.php +++ b/system/Cache/ResponseCache.php @@ -74,7 +74,7 @@ public function generateCacheKey($request): string * * @params int $ttl time to live in seconds. */ - public function cachePage($request, ResponseInterface $response, int $ttl): bool + public function make($request, ResponseInterface $response, int $ttl): bool { $headers = []; diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 5ce7ac69b2c4..ace073b5ed9e 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -526,7 +526,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache // so that we can have live speed updates along the way. // Must be run after filters to preserve the Response headers. if (static::$cacheTTL > 0) { - $this->pageCache->cachePage($this->request, $this->response, static::$cacheTTL); + $this->pageCache->make($this->request, $this->response, static::$cacheTTL); } // Update the performance metrics diff --git a/tests/system/Cache/ResponseCacheTest.php b/tests/system/Cache/ResponseCacheTest.php index 75038e2753fa..a24a91e2ea52 100644 --- a/tests/system/Cache/ResponseCacheTest.php +++ b/tests/system/Cache/ResponseCacheTest.php @@ -101,7 +101,7 @@ public function testCachePageIncomingRequest() $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $return = $pageCache->cachePage($request, $response, 300); + $return = $pageCache->make($request, $response, 300); $this->assertTrue($return); @@ -141,7 +141,7 @@ public function testCachePageIncomingRequestWithCacheQueryString() $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $return = $pageCache->cachePage($request, $response, 300); + $return = $pageCache->make($request, $response, 300); $this->assertTrue($return); @@ -176,7 +176,7 @@ public function testCachePageCLIRequest() $response = new Response($this->appConfig); $response->setBody('The response body.'); - $return = $pageCache->cachePage($request, $response, 300); + $return = $pageCache->make($request, $response, 300); $this->assertTrue($return); @@ -210,7 +210,7 @@ public function testUnserializeError() $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $pageCache->cachePage($request, $response, 300); + $pageCache->make($request, $response, 300); $cacheKey = $pageCache->generateCacheKey($request); @@ -236,7 +236,7 @@ public function testInvalidCacheError() $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $pageCache->cachePage($request, $response, 300); + $pageCache->make($request, $response, 300); $cacheKey = $pageCache->generateCacheKey($request); From 8b0230a2278d08bbaccb297197be156e254e0937 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 3 Jul 2023 11:33:53 +0900 Subject: [PATCH 311/485] refactor: rename getCachedResponse() to get() --- deptrac.yaml | 2 +- system/Cache/ResponseCache.php | 2 +- system/CodeIgniter.php | 2 +- tests/system/Cache/ResponseCacheTest.php | 20 ++++++++++---------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/deptrac.yaml b/deptrac.yaml index 96cfc8fe522a..d2db0605103b 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -222,7 +222,7 @@ parameters: - Cache skip_violations: # Individual class exemptions - CodeIgniter\Cache\PageCache: + CodeIgniter\Cache\ResponseCache: - CodeIgniter\HTTP\CLIRequest - CodeIgniter\HTTP\IncomingRequest - CodeIgniter\HTTP\ResponseInterface diff --git a/system/Cache/ResponseCache.php b/system/Cache/ResponseCache.php index 20e871cc2f3c..3328f3824e1e 100644 --- a/system/Cache/ResponseCache.php +++ b/system/Cache/ResponseCache.php @@ -94,7 +94,7 @@ public function make($request, ResponseInterface $response, int $ttl): bool * * @param CLIRequest|IncomingRequest $request */ - public function getCachedResponse($request, ResponseInterface $response): ?ResponseInterface + public function get($request, ResponseInterface $response): ?ResponseInterface { if ($cachedResponse = $this->cache->get($this->generateCacheKey($request))) { $cachedResponse = unserialize($cachedResponse); diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index ace073b5ed9e..f61e99936aa4 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -684,7 +684,7 @@ protected function forceSecureAccess($duration = 31_536_000) */ public function displayCache(Cache $config) { - if ($cachedResponse = $this->pageCache->getCachedResponse($this->request, $this->response)) { + if ($cachedResponse = $this->pageCache->get($this->request, $this->response)) { $this->response = $cachedResponse; $this->totalTime = $this->benchmark->getElapsedTime('total_execution'); diff --git a/tests/system/Cache/ResponseCacheTest.php b/tests/system/Cache/ResponseCacheTest.php index a24a91e2ea52..b97efa223233 100644 --- a/tests/system/Cache/ResponseCacheTest.php +++ b/tests/system/Cache/ResponseCacheTest.php @@ -107,7 +107,7 @@ public function testCachePageIncomingRequest() // Check cache with a request with the same URI path. $request = $this->createIncomingRequest('foo/bar'); - $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); @@ -115,7 +115,7 @@ public function testCachePageIncomingRequest() // Check cache with a request with the same URI path and different query string. $request = $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); - $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); @@ -124,7 +124,7 @@ public function testCachePageIncomingRequest() // Check cache with another request with the different URI path. $request = $this->createIncomingRequest('another'); - $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); $this->assertNull($cachedResponse); } @@ -147,7 +147,7 @@ public function testCachePageIncomingRequestWithCacheQueryString() // Check cache with a request with the same URI path and same query string. $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); - $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); @@ -155,14 +155,14 @@ public function testCachePageIncomingRequestWithCacheQueryString() // Check cache with a request with the same URI path and different query string. $request = $this->createIncomingRequest('foo/bar', ['xfoo' => 'bar', 'bar' => 'baz']); - $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); $this->assertNull($cachedResponse); // Check cache with another request with the different URI path. $request = $this->createIncomingRequest('another'); - $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); $this->assertNull($cachedResponse); } @@ -182,7 +182,7 @@ public function testCachePageCLIRequest() // Check cache with a request with the same params. $request = $this->createCLIRequest(['foo', 'bar']); - $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); @@ -190,7 +190,7 @@ public function testCachePageCLIRequest() // Check cache with another request with the different params. $request = $this->createCLIRequest(['baz']); - $cachedResponse = $pageCache->getCachedResponse($request, new Response($this->appConfig)); + $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); $this->assertNull($cachedResponse); } @@ -218,7 +218,7 @@ public function testUnserializeError() $cache->save($cacheKey, 'Invalid data'); // Check cache with a request with the same URI path. - $pageCache->getCachedResponse($request, new Response($this->appConfig)); + $pageCache->get($request, new Response($this->appConfig)); } public function testInvalidCacheError() @@ -244,6 +244,6 @@ public function testInvalidCacheError() $cache->save($cacheKey, serialize(['a' => '1'])); // Check cache with a request with the same URI path. - $pageCache->getCachedResponse($request, new Response($this->appConfig)); + $pageCache->get($request, new Response($this->appConfig)); } } From f0748dde688a8004851480f58a5c19246e3749bb Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 3 Jul 2023 11:55:04 +0900 Subject: [PATCH 312/485] refactor: add ResponseCache::$ttl and use it --- system/Cache/ResponseCache.php | 28 ++++++++++++++++++++---- system/CodeIgniter.php | 10 ++++++--- system/Controller.php | 7 +++--- tests/system/Cache/ResponseCacheTest.php | 12 +++++----- tests/system/CodeIgniterTest.php | 2 +- 5 files changed, 42 insertions(+), 17 deletions(-) diff --git a/system/Cache/ResponseCache.php b/system/Cache/ResponseCache.php index 3328f3824e1e..8a3865b3814e 100644 --- a/system/Cache/ResponseCache.php +++ b/system/Cache/ResponseCache.php @@ -37,6 +37,13 @@ class ResponseCache */ protected $cacheQueryString = false; + /** + * Cache time to live. + * + * @var int seconds + */ + protected int $ttl = 0; + protected CacheInterface $cache; public function __construct(CacheConfig $config, CacheInterface $cache) @@ -45,6 +52,21 @@ public function __construct(CacheConfig $config, CacheInterface $cache) $this->cache = $cache; } + public function getTtl(): int + { + return $this->ttl; + } + + /** + * @return $this + */ + public function setTtl(int $ttl) + { + $this->ttl = $ttl; + + return $this; + } + /** * Generates the cache key to use from the current request. * @@ -71,10 +93,8 @@ public function generateCacheKey($request): string * Caches the full response from the current request. * * @param CLIRequest|IncomingRequest $request - * - * @params int $ttl time to live in seconds. */ - public function make($request, ResponseInterface $response, int $ttl): bool + public function make($request, ResponseInterface $response): bool { $headers = []; @@ -85,7 +105,7 @@ public function make($request, ResponseInterface $response, int $ttl): bool return $this->cache->save( $this->generateCacheKey($request), serialize(['headers' => $headers, 'output' => $response->getBody()]), - $ttl + $this->ttl ); } diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index f61e99936aa4..20d788132677 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -128,6 +128,8 @@ class CodeIgniter * Cache expiration time * * @var int seconds + * + * @deprecated 4.4.0 Moved to ResponseCache::$ttl. No longer used. */ protected static $cacheTTL = 0; @@ -338,7 +340,7 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon ); } - static::$cacheTTL = 0; + $this->pageCache->setTtl(0); $this->bufferLevel = ob_get_level(); $this->startBenchmark(); @@ -525,8 +527,8 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache // Cache it without the performance metrics replaced // so that we can have live speed updates along the way. // Must be run after filters to preserve the Response headers. - if (static::$cacheTTL > 0) { - $this->pageCache->make($this->request, $this->response, static::$cacheTTL); + if ($this->pageCache->getTtl() > 0) { + $this->pageCache->make($this->request, $this->response); } // Update the performance metrics @@ -699,6 +701,8 @@ public function displayCache(Cache $config) /** * Tells the app that the final output should be cached. + * + * @deprecated 4.4.0 Moved to ResponseCache::setTtl(). to No longer used. */ public static function cache(int $time) { diff --git a/system/Controller.php b/system/Controller.php index 64de91ab2938..e808e0b8c20f 100644 --- a/system/Controller.php +++ b/system/Controller.php @@ -104,12 +104,13 @@ protected function forceHTTPS(int $duration = 31_536_000) } /** - * Provides a simple way to tie into the main CodeIgniter class and - * tell it how long to cache the current page for. + * How long to cache the current page for. + * + * @params int $time time to live in seconds. */ protected function cachePage(int $time) { - CodeIgniter::cache($time); + Services::responsecache()->setTtl($time); } /** diff --git a/tests/system/Cache/ResponseCacheTest.php b/tests/system/Cache/ResponseCacheTest.php index b97efa223233..a1079e63a463 100644 --- a/tests/system/Cache/ResponseCacheTest.php +++ b/tests/system/Cache/ResponseCacheTest.php @@ -88,7 +88,7 @@ private function createResponseCache(?CacheConfig $cacheConfig = null): Response $cacheConfig ??= new CacheConfig(); - return new ResponseCache($cacheConfig, $cache); + return (new ResponseCache($cacheConfig, $cache))->setTtl(300); } public function testCachePageIncomingRequest() @@ -101,7 +101,7 @@ public function testCachePageIncomingRequest() $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $return = $pageCache->make($request, $response, 300); + $return = $pageCache->make($request, $response); $this->assertTrue($return); @@ -141,7 +141,7 @@ public function testCachePageIncomingRequestWithCacheQueryString() $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $return = $pageCache->make($request, $response, 300); + $return = $pageCache->make($request, $response); $this->assertTrue($return); @@ -176,7 +176,7 @@ public function testCachePageCLIRequest() $response = new Response($this->appConfig); $response->setBody('The response body.'); - $return = $pageCache->make($request, $response, 300); + $return = $pageCache->make($request, $response); $this->assertTrue($return); @@ -210,7 +210,7 @@ public function testUnserializeError() $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $pageCache->make($request, $response, 300); + $pageCache->make($request, $response); $cacheKey = $pageCache->generateCacheKey($request); @@ -236,7 +236,7 @@ public function testInvalidCacheError() $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $pageCache->make($request, $response, 300); + $pageCache->make($request, $response); $cacheKey = $pageCache->generateCacheKey($request); diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index b2430a26a7c8..25ee4eda832a 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -838,7 +838,7 @@ public function testPageCacheWithCacheQueryString( $routePath = explode('?', $testingUrl)[0]; $string = 'This is a test page, to check cache configuration'; $routes->add($routePath, static function () use ($string) { - CodeIgniter::cache(60); + Services::responsecache()->setTtl(60); $response = Services::response(); return $response->setBody($string); From fd4e951280dd21cf6900ccd891899043d895d757 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 3 Jul 2023 12:42:27 +0900 Subject: [PATCH 313/485] test: set $_SERVER['SCRIPT_NAME'] --- tests/system/CodeIgniterTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 25ee4eda832a..584310ba08c6 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -96,6 +96,7 @@ public function testRunClosureRoute() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/pages/about'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; // Inject mock router. $routes = Services::routes(); @@ -193,6 +194,7 @@ public function testControllersCanReturnString() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/pages/about'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; // Inject mock router. $routes = Services::routes(); @@ -216,6 +218,7 @@ public function testControllersCanReturnResponseObject() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/pages/about'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; // Inject mock router. $routes = Services::routes(); @@ -244,6 +247,7 @@ public function testControllersCanReturnDownloadResponseObject() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/pages/about'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; // Inject mock router. $routes = Services::routes(); @@ -268,6 +272,7 @@ public function testRunExecuteFilterByClassName() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/pages/about'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; // Inject mock router. $routes = Services::routes(); @@ -298,6 +303,7 @@ public function testRegisterSameFilterTwiceWithDifferentArgument() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/pages/about'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; $routes = Services::routes(); $routes->add( @@ -331,6 +337,7 @@ public function testDisableControllerFilters() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/pages/about'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; // Inject mock router. $routes = Services::routes(); @@ -452,6 +459,7 @@ public function testRunRedirectionWithNamed() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/example'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; // Inject mock router. $routes = Services::routes(); @@ -475,6 +483,7 @@ public function testRunRedirectionWithURI() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/example'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; // Inject mock router. $routes = Services::routes(); @@ -501,6 +510,7 @@ public function testRunRedirectionWithGET() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/example'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; $_SERVER['REQUEST_METHOD'] = 'GET'; @@ -527,6 +537,7 @@ public function testRunRedirectionWithGETAndHTTPCode301() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/example'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; $_SERVER['REQUEST_METHOD'] = 'GET'; @@ -551,6 +562,7 @@ public function testRunRedirectionWithPOSTAndHTTPCode301() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/example'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; $_SERVER['REQUEST_METHOD'] = 'POST'; @@ -615,6 +627,7 @@ public function testNotStoresPreviousURL() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/example'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; $_SERVER['REQUEST_METHOD'] = 'GET'; @@ -638,6 +651,7 @@ public function testNotStoresPreviousURLByCheckingContentType() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/image'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; // Inject mock router. $routes = Services::routes(); @@ -679,6 +693,7 @@ public function testRunCLIRoute() $_SERVER['argc'] = 2; $_SERVER['REQUEST_URI'] = '/cli'; + $_SERVER['SCRIPT_NAME'] = 'public/index.php'; $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; $_SERVER['REQUEST_METHOD'] = 'CLI'; @@ -698,6 +713,7 @@ public function testSpoofRequestMethodCanUsePUT() $_SERVER['argc'] = 1; $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; $_SERVER['REQUEST_METHOD'] = 'POST'; @@ -722,6 +738,7 @@ public function testSpoofRequestMethodCannotUseGET() $_SERVER['argc'] = 1; $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; $_SERVER['REQUEST_METHOD'] = 'POST'; @@ -754,6 +771,7 @@ public function testPageCacheSendSecureHeaders() command('cache:clear'); $_SERVER['REQUEST_URI'] = '/test'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; $routes = Services::routes(); $routes->add('test', static function () { @@ -832,6 +850,7 @@ public function testPageCacheWithCacheQueryString( foreach ($testingUrls as $testingUrl) { $this->resetServices(); $_SERVER['REQUEST_URI'] = '/' . $testingUrl; + $_SERVER['SCRIPT_NAME'] = '/index.php'; $this->codeigniter = new MockCodeIgniter(new App()); $routes = Services::routes(true); From cd5b817cc08deb0666e316f9f5fe24da50d40ae2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 3 Jul 2023 13:38:35 +0900 Subject: [PATCH 314/485] refactor: don't make cahce if $ttl is 0 --- system/Cache/ResponseCache.php | 11 +++++------ system/CodeIgniter.php | 4 +--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/system/Cache/ResponseCache.php b/system/Cache/ResponseCache.php index 8a3865b3814e..b4c2fe9187fa 100644 --- a/system/Cache/ResponseCache.php +++ b/system/Cache/ResponseCache.php @@ -52,11 +52,6 @@ public function __construct(CacheConfig $config, CacheInterface $cache) $this->cache = $cache; } - public function getTtl(): int - { - return $this->ttl; - } - /** * @return $this */ @@ -90,12 +85,16 @@ public function generateCacheKey($request): string } /** - * Caches the full response from the current request. + * Caches the response. * * @param CLIRequest|IncomingRequest $request */ public function make($request, ResponseInterface $response): bool { + if ($this->ttl === 0) { + return true; + } + $headers = []; foreach ($response->headers() as $header) { diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 20d788132677..2ff8da02d456 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -527,9 +527,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache // Cache it without the performance metrics replaced // so that we can have live speed updates along the way. // Must be run after filters to preserve the Response headers. - if ($this->pageCache->getTtl() > 0) { - $this->pageCache->make($this->request, $this->response); - } + $this->pageCache->make($this->request, $this->response); // Update the performance metrics $body = $this->response->getBody(); From eb288ec342262ac3ced5ad09a38184c14a176230 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 3 Jul 2023 14:13:53 +0900 Subject: [PATCH 315/485] docs: group items of the same class --- user_guide_src/source/changelogs/v4.4.0.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index c686450ef864..f8b3768a78ef 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -52,11 +52,12 @@ Parameter Type Changes - **Services:** The first parameter of ``Services::security()`` has been changed from ``Config\App`` to ``Config\Security``. -- **Session:** The second parameter of ``Session::__construct()`` has been - changed from ``Config\App`` to ``Config\Session``. -- **Session:** The first parameter of ``__construct()`` in ``BaseHandler``, - ``DatabaseHandler``, ``FileHandler``, ``MemcachedHandler``, and ``RedisHandler`` - has been changed from ``Config\App`` to ``Config\Session``. +- **Session:** + - The second parameter of ``Session::__construct()`` has been changed from + ``Config\App`` to ``Config\Session``. + - The first parameter of ``__construct()`` in ``BaseHandler``, + ``DatabaseHandler``, ``FileHandler``, ``MemcachedHandler``, and ``RedisHandler`` + has been changed from ``Config\App`` to ``Config\Session``. - **Security:** The first parameter of ``Security::__construct()`` has been changed from ``Config\App`` to ``Config\Security``. - **Validation:** The method signature of ``Validation::check()`` has been changed. From 7d31f522c78c7ff935a077edad473b46439f543f Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 3 Jul 2023 14:14:27 +0900 Subject: [PATCH 316/485] docs: add "Parameter Type Changes" and "Deprecations" --- user_guide_src/source/changelogs/v4.4.0.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index f8b3768a78ef..b101decaad4d 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -62,6 +62,14 @@ Parameter Type Changes changed from ``Config\App`` to ``Config\Security``. - **Validation:** The method signature of ``Validation::check()`` has been changed. The ``string`` typehint on the ``$rule`` parameter was removed. +- **CodeIgniter:** The method signature of ``CodeIgniter::setRequest()`` has been + changed. The ``Request`` typehint on the ``$request`` parameter was removed. +- **FeatureTestCase:** + - The method signature of ``FeatureTestCase::populateGlobals()`` has been + changed. The ``Request`` typehint on the ``$request`` parameter was removed. + - The method signature of ``FeatureTestCase::setRequestBody()`` has been + changed. The ``Request`` typehint on the ``$request`` parameter and the + return type ``Request`` were removed. Added Parameters ---------------- @@ -180,7 +188,12 @@ Deprecations are deprecated. Because these methods have been moved to ``BaseExceptionHandler`` or ``ExceptionHandler``. - **Autoloader:** ``Autoloader::sanitizeFilename()`` is deprecated. -- **CodeIgniter:** ``CodeIgniter::$returnResponse`` property is deprecated. No longer used. +- **CodeIgniter:** + - ``CodeIgniter::$returnResponse`` property is deprecated. No longer used. + - ``CodeIgniter::$cacheTTL`` property is deprecated. No longer used. Use ``ResponseCache`` instead. + - ``CodeIgniter::cache()`` method is deprecated. No longer used. Use ``ResponseCache`` instead. + - ``CodeIgniter::cachePage()`` method is deprecated. No longer used. Use ``ResponseCache`` instead. + - ``CodeIgniter::generateCacheName()`` method is deprecated. No longer used. Use ``ResponseCache`` instead. - **RedirectException:** ``\CodeIgniter\Router\Exceptions\RedirectException`` is deprecated. Use ``\CodeIgniter\HTTP\Exceptions\RedirectException`` instead. - **Session:** The property ``$sessionDriverName``, ``$sessionCookieName``, ``$sessionExpiration``, ``$sessionSavePath``, ``$sessionMatchIP``, From 42ea49371ad78be79c0d319385a376c2013edac3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Jul 2023 09:46:23 +0900 Subject: [PATCH 317/485] docs: fix by proofreading Co-authored-by: MGatner --- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index b101decaad4d..8bf257532fb4 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -55,7 +55,7 @@ Parameter Type Changes - **Session:** - The second parameter of ``Session::__construct()`` has been changed from ``Config\App`` to ``Config\Session``. - - The first parameter of ``__construct()`` in ``BaseHandler``, + - The first parameter of ``__construct()`` in the database's ``BaseHandler``, ``DatabaseHandler``, ``FileHandler``, ``MemcachedHandler``, and ``RedisHandler`` has been changed from ``Config\App`` to ``Config\Session``. - **Security:** The first parameter of ``Security::__construct()`` has been From d19a39306256d7598a0e9cec2095413fb275cbdd Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Jul 2023 11:16:46 +0900 Subject: [PATCH 318/485] feat: add DefinedRouteCollector --- system/Router/DefinedRouteCollector.php | 63 +++++++++++++ .../Router/DefinedRouteCollectorTest.php | 90 +++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 system/Router/DefinedRouteCollector.php create mode 100644 tests/system/Router/DefinedRouteCollectorTest.php diff --git a/system/Router/DefinedRouteCollector.php b/system/Router/DefinedRouteCollector.php new file mode 100644 index 000000000000..f9e0454b7b07 --- /dev/null +++ b/system/Router/DefinedRouteCollector.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Router; + +use Closure; +use Generator; + +class DefinedRouteCollector +{ + private RouteCollection $routeCollection; + + public function __construct(RouteCollection $routes) + { + $this->routeCollection = $routes; + } + + public function collect(): Generator + { + $methods = [ + 'get', + 'head', + 'post', + 'patch', + 'put', + 'delete', + 'options', + 'trace', + 'connect', + 'cli', + ]; + + foreach ($methods as $method) { + $routes = $this->routeCollection->getRoutes($method); + + foreach ($routes as $route => $handler) { + if (is_string($handler) || $handler instanceof Closure) { + + if ($handler instanceof Closure) { + $handler = '(Closure)'; + } + + $routeName = $this->routeCollection->getRoutesOptions($route)['as'] ?? $route; + + yield [ + 'method' => $method, + 'route' => $route, + 'name' => $routeName, + 'handler' => $handler, + ]; + } + } + } + } +} diff --git a/tests/system/Router/DefinedRouteCollectorTest.php b/tests/system/Router/DefinedRouteCollectorTest.php new file mode 100644 index 000000000000..61647967766c --- /dev/null +++ b/tests/system/Router/DefinedRouteCollectorTest.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Router; + +use CodeIgniter\Config\Services; +use CodeIgniter\Test\CIUnitTestCase; +use Config\Modules; +use Config\Routing; + +/** + * @internal + * + * @group Others + */ +final class DefinedRouteCollectorTest extends CIUnitTestCase +{ + private function createRouteCollection(array $config = [], $moduleConfig = null): RouteCollection + { + $defaults = [ + 'Config' => APPPATH . 'Config', + 'App' => APPPATH, + ]; + $config = array_merge($config, $defaults); + + Services::autoloader()->addNamespace($config); + + $loader = Services::locator(); + + if ($moduleConfig === null) { + $moduleConfig = new Modules(); + $moduleConfig->enabled = false; + } + + return (new RouteCollection($loader, $moduleConfig, new Routing()))->setHTTPVerb('get'); + } + + public function test() + { + $routes = $this->createRouteCollection(); + $routes->get('journals', 'Blogs'); + $routes->get('product/(:num)', 'Catalog::productLookupByID/$1'); + $routes->get('feed', static fn () => 'A Closure route.'); + $routes->view('about', 'pages/about'); + + $collector = new DefinedRouteCollector($routes); + + $definedRoutes = []; + + foreach ($collector->collect() as $route) { + $definedRoutes[] = $route; + } + + $expected = [ + [ + 'method' => 'get', + 'route' => 'journals', + 'name' => 'journals', + 'handler' => '\App\Controllers\Blogs', + ], + [ + 'method' => 'get', + 'route' => 'product/([0-9]+)', + 'name' => 'product/([0-9]+)', + 'handler' => '\App\Controllers\Catalog::productLookupByID/$1', + ], + [ + 'method' => 'get', + 'route' => 'feed', + 'name' => 'feed', + 'handler' => '(Closure)', + ], + [ + 'method' => 'get', + 'route' => 'about', + 'name' => 'about', + 'handler' => '(Closure)', + ], + ]; + $this->assertSame($expected, $definedRoutes); + } +} From d1224ccd97760e94463ec9288da50b98f207823b Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Jul 2023 11:31:17 +0900 Subject: [PATCH 319/485] refactor: use DefinedRouteCollector in `spark routes` --- system/Commands/Utilities/Routes.php | 35 ++++++++++++---------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index 97b990bb1092..68d2f8d07d50 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -18,6 +18,7 @@ use CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\AutoRouteCollector as AutoRouteCollectorImproved; use CodeIgniter\Commands\Utilities\Routes\FilterCollector; use CodeIgniter\Commands\Utilities\Routes\SampleURIGenerator; +use CodeIgniter\Router\DefinedRouteCollector; use Config\Feature; use Config\Routing; use Config\Services; @@ -115,29 +116,23 @@ public function run(array $params) $uriGenerator = new SampleURIGenerator(); $filterCollector = new FilterCollector(); - foreach ($methods as $method) { - $routes = $collection->getRoutes($method); + $definedRouteCollector = new DefinedRouteCollector($collection); - foreach ($routes as $route => $handler) { - if (is_string($handler) || $handler instanceof Closure) { - $sampleUri = $uriGenerator->get($route); - $filters = $filterCollector->get($method, $sampleUri); + foreach ($definedRouteCollector->collect() as $route) { + if (is_string($route['handler']) || $route['handler'] instanceof Closure) { + $sampleUri = $uriGenerator->get($route['route']); + $filters = $filterCollector->get($route['method'], $sampleUri); - if ($handler instanceof Closure) { - $handler = '(Closure)'; - } - - $routeName = $collection->getRoutesOptions($route)['as'] ?? '»'; + $routeName = ($route['route'] === $route['name']) ? '»' : $route['route']; - $tbody[] = [ - strtoupper($method), - $route, - $routeName, - $handler, - implode(' ', array_map('class_basename', $filters['before'])), - implode(' ', array_map('class_basename', $filters['after'])), - ]; - } + $tbody[] = [ + strtoupper($route['method']), + $route['route'], + $routeName, + $route['handler'], + implode(' ', array_map('class_basename', $filters['before'])), + implode(' ', array_map('class_basename', $filters['after'])), + ]; } } From 06faf737beb08996b234719ba92c6ecd4c2e8b79 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Jul 2023 11:51:32 +0900 Subject: [PATCH 320/485] refactor: use DefinedRouteCollector in DebugBar --- system/Debug/Toolbar/Collectors/Routes.php | 42 +++++++--------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/system/Debug/Toolbar/Collectors/Routes.php b/system/Debug/Toolbar/Collectors/Routes.php index 5ea88c0411c8..0420c19b92dd 100644 --- a/system/Debug/Toolbar/Collectors/Routes.php +++ b/system/Debug/Toolbar/Collectors/Routes.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Debug\Toolbar\Collectors; +use CodeIgniter\Router\DefinedRouteCollector; use Config\Services; use ReflectionException; use ReflectionFunction; @@ -55,9 +56,6 @@ public function display(): array $rawRoutes = Services::routes(true); $router = Services::router(null, null, true); - // Matched Route - $route = $router->getMatchedRoute(); - // Get our parameters // Closure routes if (is_callable($router->controllerName())) { @@ -100,32 +98,18 @@ public function display(): array ]; // Defined Routes - $routes = []; - $methods = [ - 'get', - 'head', - 'post', - 'patch', - 'put', - 'delete', - 'options', - 'trace', - 'connect', - 'cli', - ]; - - foreach ($methods as $method) { - $raw = $rawRoutes->getRoutes($method); - - foreach ($raw as $route => $handler) { - // filter for strings, as callbacks aren't displayable - if (is_string($handler)) { - $routes[] = [ - 'method' => strtoupper($method), - 'route' => $route, - 'handler' => $handler, - ]; - } + $routes = []; + + $definedRouteCollector = new DefinedRouteCollector($rawRoutes); + + foreach ($definedRouteCollector->collect() as $route) { + // filter for strings, as callbacks aren't displayable + if ($route['handler'] !== '(Closure)') { + $routes[] = [ + 'method' => strtoupper($route['method']), + 'route' => $route['route'], + 'handler' => $route['handler'], + ]; } } From 6f0d950c33a9a7d2b4c1d98fd6b4313098ee56aa Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Jul 2023 12:03:19 +0900 Subject: [PATCH 321/485] refactor: use DefinedRouteCollector in `spark routes` --- system/Commands/Utilities/Routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index 68d2f8d07d50..ee726d4aa93d 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -123,7 +123,7 @@ public function run(array $params) $sampleUri = $uriGenerator->get($route['route']); $filters = $filterCollector->get($route['method'], $sampleUri); - $routeName = ($route['route'] === $route['name']) ? '»' : $route['route']; + $routeName = ($route['route'] === $route['name']) ? '»' : $route['name']; $tbody[] = [ strtoupper($route['method']), From 453511c582b2ee3829be115f4b83abcc29441e49 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Jul 2023 12:08:14 +0900 Subject: [PATCH 322/485] docs: add comment --- system/Router/DefinedRouteCollector.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/Router/DefinedRouteCollector.php b/system/Router/DefinedRouteCollector.php index f9e0454b7b07..c1d98a5ea95b 100644 --- a/system/Router/DefinedRouteCollector.php +++ b/system/Router/DefinedRouteCollector.php @@ -14,6 +14,9 @@ use Closure; use Generator; +/** + * Collect all defined routes for display. + */ class DefinedRouteCollector { private RouteCollection $routeCollection; From ea62e0b022dccda589aef1333b6bf36166c6ba0c Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Jul 2023 18:30:52 +0900 Subject: [PATCH 323/485] fix: add final keyword to DefinedRouteCollector --- system/Router/DefinedRouteCollector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Router/DefinedRouteCollector.php b/system/Router/DefinedRouteCollector.php index c1d98a5ea95b..62aebce51ba0 100644 --- a/system/Router/DefinedRouteCollector.php +++ b/system/Router/DefinedRouteCollector.php @@ -17,7 +17,7 @@ /** * Collect all defined routes for display. */ -class DefinedRouteCollector +final class DefinedRouteCollector { private RouteCollection $routeCollection; From f6e636256ea157c4818e6f93fbacb8c256a65e4b Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Jul 2023 18:31:58 +0900 Subject: [PATCH 324/485] test: fix test method name --- tests/system/Router/DefinedRouteCollectorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Router/DefinedRouteCollectorTest.php b/tests/system/Router/DefinedRouteCollectorTest.php index 61647967766c..619f4386886f 100644 --- a/tests/system/Router/DefinedRouteCollectorTest.php +++ b/tests/system/Router/DefinedRouteCollectorTest.php @@ -43,7 +43,7 @@ private function createRouteCollection(array $config = [], $moduleConfig = null) return (new RouteCollection($loader, $moduleConfig, new Routing()))->setHTTPVerb('get'); } - public function test() + public function testCollect() { $routes = $this->createRouteCollection(); $routes->get('journals', 'Blogs'); From f5a284154c78d68cbe85e9ad76f82e0f85e2b3d3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Jul 2023 18:48:26 +0900 Subject: [PATCH 325/485] docs: add @phpstan-return --- system/Router/DefinedRouteCollector.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/Router/DefinedRouteCollector.php b/system/Router/DefinedRouteCollector.php index 62aebce51ba0..eeae28379d04 100644 --- a/system/Router/DefinedRouteCollector.php +++ b/system/Router/DefinedRouteCollector.php @@ -26,6 +26,9 @@ public function __construct(RouteCollection $routes) $this->routeCollection = $routes; } + /** + * @phpstan-return Generator + */ public function collect(): Generator { $methods = [ From 789015666afae712308721b12010b91021044074 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 5 Jul 2023 07:38:13 +0900 Subject: [PATCH 326/485] refactor: make ResponseCache final --- system/Cache/ResponseCache.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system/Cache/ResponseCache.php b/system/Cache/ResponseCache.php index b4c2fe9187fa..44da42947633 100644 --- a/system/Cache/ResponseCache.php +++ b/system/Cache/ResponseCache.php @@ -20,7 +20,7 @@ /** * Web Page Caching */ -class ResponseCache +final class ResponseCache { /** * Whether to take the URL query string into consideration when generating @@ -35,16 +35,16 @@ class ResponseCache * * @var bool|string[] */ - protected $cacheQueryString = false; + private $cacheQueryString = false; /** * Cache time to live. * * @var int seconds */ - protected int $ttl = 0; + private int $ttl = 0; - protected CacheInterface $cache; + private CacheInterface $cache; public function __construct(CacheConfig $config, CacheInterface $cache) { From a275140b536893d53ee40c8e6bb74546c4265fb7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 5 Jul 2023 11:03:52 +0900 Subject: [PATCH 327/485] fix: update variable name in 4.4 See https://github.com/codeigniter4/CodeIgniter4/pull/7652 --- system/Router/RouteCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 28b3a1779ab4..21fa22dda707 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1328,7 +1328,7 @@ protected function buildReverseRoute(string $routeKey, array $params): string foreach ($matches[0] as $index => $pattern) { if (! isset($params[$index])) { throw new InvalidArgumentException( - 'Missing argument for "' . $pattern . '" in route "' . $from . '".' + 'Missing argument for "' . $pattern . '" in route "' . $routeKey . '".' ); } if (! preg_match('#^' . $pattern . '$#u', $params[$index])) { From 6878e43f27fe808fdfe9e42b1fee4641e85a8e4b Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 6 Jul 2023 13:46:40 +0900 Subject: [PATCH 328/485] refactor: remove uneeded `if` --- system/Commands/Utilities/Routes.php | 29 +++++++++++++--------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index ee726d4aa93d..b3693ec6860e 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -11,7 +11,6 @@ namespace CodeIgniter\Commands\Utilities; -use Closure; use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use CodeIgniter\Commands\Utilities\Routes\AutoRouteCollector; @@ -119,21 +118,19 @@ public function run(array $params) $definedRouteCollector = new DefinedRouteCollector($collection); foreach ($definedRouteCollector->collect() as $route) { - if (is_string($route['handler']) || $route['handler'] instanceof Closure) { - $sampleUri = $uriGenerator->get($route['route']); - $filters = $filterCollector->get($route['method'], $sampleUri); - - $routeName = ($route['route'] === $route['name']) ? '»' : $route['name']; - - $tbody[] = [ - strtoupper($route['method']), - $route['route'], - $routeName, - $route['handler'], - implode(' ', array_map('class_basename', $filters['before'])), - implode(' ', array_map('class_basename', $filters['after'])), - ]; - } + $sampleUri = $uriGenerator->get($route['route']); + $filters = $filterCollector->get($route['method'], $sampleUri); + + $routeName = ($route['route'] === $route['name']) ? '»' : $route['name']; + + $tbody[] = [ + strtoupper($route['method']), + $route['route'], + $routeName, + $route['handler'], + implode(' ', array_map('class_basename', $filters['before'])), + implode(' ', array_map('class_basename', $filters['after'])), + ]; } if ($collection->shouldAutoRoute()) { From ef1b3b230a0b8bfaefbd0e0c0f3ac1d9c59c106d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Jul 2023 08:35:19 +0900 Subject: [PATCH 329/485] docs: add @phpstan-var to stop rector to skip RemoveAlwaysTrueIfConditionRector Without this doc type, Rector will refactor the following: 1) system/Router/AutoRouterImproved.php:294 ---------- begin diff ---------- @@ @@ // Update the positions. $this->methodPos = $this->paramPos; - if ($params === []) { - $this->paramPos = null; - } + $this->paramPos = null; if ($this->paramPos !== null) { $this->paramPos++; } ----------- end diff ----------- Applied rules: * RemoveAlwaysTrueIfConditionRector --- system/Router/AutoRouterImproved.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index b8c1e9d35099..a186fd33ef55 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -46,6 +46,8 @@ final class AutoRouterImproved implements AutoRouterInterface /** * An array of params to the controller method. + * + * @phpstan-var list */ private array $params = []; @@ -72,6 +74,8 @@ final class AutoRouterImproved implements AutoRouterInterface /** * The URI segments. + * + * @phpstan-var list */ private array $segments = []; @@ -279,6 +283,7 @@ public function getRoute(string $uri, string $httpVerb): array } // The first item may be a method name. + /** @phpstan-var list $params */ $params = $this->params; $methodParam = array_shift($params); From c009117834c2f249a6a444cb0a41a85cba5e3f15 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 3 Jul 2023 19:30:03 +0900 Subject: [PATCH 330/485] docs: fix @phpstan-return --- system/Router/RouteCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 21fa22dda707..5e4cdb36ef6e 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1649,7 +1649,7 @@ public function resetRoutes() * array{ * filter?: string|list, namespace?: string, hostname?: string, * subdomain?: string, offset?: int, priority?: int, as?: string, - * redirect?: string + * redirect?: int * } * > */ From 3f3d2162367f613401f4e5f64de50f2e0d79788c Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Jul 2023 13:12:54 +0900 Subject: [PATCH 331/485] feat: improve `spark routes` output for View routes --- system/Router/DefinedRouteCollector.php | 4 +++- system/Router/RouteCollection.php | 5 ++++- tests/system/Router/DefinedRouteCollectorTest.php | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/system/Router/DefinedRouteCollector.php b/system/Router/DefinedRouteCollector.php index eeae28379d04..f77383fa6cef 100644 --- a/system/Router/DefinedRouteCollector.php +++ b/system/Router/DefinedRouteCollector.php @@ -51,7 +51,9 @@ public function collect(): Generator if (is_string($handler) || $handler instanceof Closure) { if ($handler instanceof Closure) { - $handler = '(Closure)'; + $view = $this->routeCollection->getRoutesOptions($route, $method)['view'] ?? false; + + $handler = $view ? '(View) ' . $view : '(Closure)'; } $routeName = $this->routeCollection->getRoutesOptions($route)['as'] ?? $route; diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 5e4cdb36ef6e..3e45d6b95588 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1101,7 +1101,10 @@ public function view(string $from, string $view, ?array $options = null): RouteC ->setData(['segments' => $data], 'raw') ->render($view, $options); - $this->create('get', $from, $to, $options); + $routeOptions = $options ?? []; + $routeOptions = array_merge($routeOptions, ['view' => $view]); + + $this->create('get', $from, $to, $routeOptions); return $this; } diff --git a/tests/system/Router/DefinedRouteCollectorTest.php b/tests/system/Router/DefinedRouteCollectorTest.php index 619f4386886f..ba74cec1a86a 100644 --- a/tests/system/Router/DefinedRouteCollectorTest.php +++ b/tests/system/Router/DefinedRouteCollectorTest.php @@ -82,7 +82,7 @@ public function testCollect() 'method' => 'get', 'route' => 'about', 'name' => 'about', - 'handler' => '(Closure)', + 'handler' => '(View) pages/about', ], ]; $this->assertSame($expected, $definedRoutes); From 0aa5dbd1dd5cfecdc64a7d06a24c5a1caadcfad8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 5 Jul 2023 08:09:59 +0900 Subject: [PATCH 332/485] docs: add changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 20db5a2d7b38..6b77eac7ecc9 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -83,8 +83,17 @@ Enhancements Commands ======== -- Now ``spark routes`` command can specify the host in the request URL. - See :ref:`routing-spark-routes-specify-host`. +- **spark routes:** + - Now you can specify the host in the request URL. + See :ref:`routing-spark-routes-specify-host`. + - It shows view files of :ref:`view-routes` in *Handler* like the following: + + +---------+-------------+------+------------------------------+----------------+---------------+ + | Method | Route | Name | Handler | Before Filters | After Filters | + +---------+-------------+------+------------------------------+----------------+---------------+ + | GET | about | » | (View) pages/about | | toolbar | + +---------+-------------+------+------------------------------+----------------+---------------+ + Testing ======= @@ -151,6 +160,7 @@ Others - **Error Handling:** Now you can use :ref:`custom-exception-handlers`. - **RedirectException:** can also take an object that implements ResponseInterface as its first argument. - **RedirectException:** implements ResponsableInterface. +- **DebugBar:** Now :ref:`view-routes` are displayed in *DEFINED ROUTES* on the *Routes* tab. Message Changes *************** From 62e20d83e8386a5ad48e42f98ee93433ffd10fc9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Jul 2023 09:26:03 +0900 Subject: [PATCH 333/485] fix: Services::session() $config type --- system/Config/Services.php | 9 +++------ tests/system/Config/ServicesTest.php | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/system/Config/Services.php b/system/Config/Services.php index c8ded8034257..a2f6a5cb03a9 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -661,19 +661,16 @@ public static function security(?SecurityConfig $config = null, bool $getShared * Return the session manager. * * @return Session - * - * @TODO replace the first parameter type `?App` with `?SessionConfig` */ - public static function session(?App $config = null, bool $getShared = true) + public static function session(?SessionConfig $config = null, bool $getShared = true) { if ($getShared) { return static::getSharedInstance('session', $config); } - $logger = AppServices::logger(); + $config ??= config(SessionConfig::class); - $config = config(SessionConfig::class); - assert($config instanceof SessionConfig, 'Missing "Config/Session.php".'); + $logger = AppServices::logger(); $driverName = $config->driver; diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index b6ea85e6971f..283233d8b4ed 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -243,7 +243,7 @@ public function testNewViewcell() */ public function testNewSession() { - $actual = Services::session($this->config); + $actual = Services::session(); $this->assertInstanceOf(Session::class, $actual); } From 3eab5e043e7ac43ec5de9fb2b85f8fb797d36657 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Jul 2023 09:28:12 +0900 Subject: [PATCH 334/485] docs: add changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 20db5a2d7b38..e6d6d6a68a63 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -50,8 +50,11 @@ Method Signature Changes Parameter Type Changes ---------------------- -- **Services:** The first parameter of ``Services::security()`` has been - changed from ``Config\App`` to ``Config\Security``. +- **Services:** + - The first parameter of ``Services::security()`` has been changed from + ``Config\App`` to ``Config\Security``. + - The first parameter of ``Services::session()`` has been changed from + ``Config\App`` to ``Config\Session``. - **Session:** - The second parameter of ``Session::__construct()`` has been changed from ``Config\App`` to ``Config\Session``. From e0f4094660561459ada5359ebd78934900f0d569 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Jul 2023 09:39:27 +0900 Subject: [PATCH 335/485] test: remove unused property --- tests/system/Config/ServicesTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 283233d8b4ed..349b6b0cc575 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -54,7 +54,6 @@ */ final class ServicesTest extends CIUnitTestCase { - private App $config; private array $original; protected function setUp(): void @@ -62,7 +61,6 @@ protected function setUp(): void parent::setUp(); $this->original = $_SERVER; - $this->config = new App(); } protected function tearDown(): void From 7ac527d72eb718406ea1a904e12fd2e37bc4b7e8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 14 Feb 2023 17:25:02 +0900 Subject: [PATCH 336/485] feat: add SiteURI class --- system/HTTP/SiteURI.php | 312 ++++++++++++++++++++++++++++++ system/HTTP/URI.php | 2 + tests/system/HTTP/SiteURITest.php | 245 +++++++++++++++++++++++ 3 files changed, 559 insertions(+) create mode 100644 system/HTTP/SiteURI.php create mode 100644 tests/system/HTTP/SiteURITest.php diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php new file mode 100644 index 000000000000..bf3e06903188 --- /dev/null +++ b/system/HTTP/SiteURI.php @@ -0,0 +1,312 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use BadMethodCallException; +use CodeIgniter\HTTP\Exceptions\HTTPException; +use Config\App; + +/** + * URI for the application site + */ +class SiteURI extends URI +{ + /** + * The baseURL. + */ + private string $baseURL; + + /** + * The Index File. + */ + private string $indexPage; + + /** + * List of URI segments in baseURL and indexPage. + * + * If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b", + * and the baseUR is "http://localhost:8888/ci431/public/", then: + * $baseSegments = [ + * 0 => 'ci431', + * 1 => 'public', + * 2 => 'index.php', + * ]; + */ + private array $baseSegments; + + /** + * List of URI segments after indexPage. + * + * The word "URI Segments" originally means only the URI path part relative + * to the baseURL. + * + * If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b", + * and the baseUR is "http://localhost:8888/ci431/public/", then: + * $segments = [ + * 0 => 'test', + * ]; + * + * @var array + * + * @deprecated This property will be private. + */ + protected $segments; + + /** + * URI path relative to baseURL. + * + * If the baseURL contains sub folders, this value will be different from + * the current URI path. + */ + private string $routePath; + + public function __construct(App $configApp) + { + // It's possible the user forgot a trailing slash on their + // baseURL, so let's help them out. + $baseURL = rtrim($configApp->baseURL, '/ ') . '/'; + + $this->baseURL = $baseURL; + $this->indexPage = $configApp->indexPage; + + $this->setBaseSeegments(); + + // Check for an index page + $indexPage = ''; + if ($configApp->indexPage !== '') { + $indexPage = $configApp->indexPage . '/'; + } + + $tempUri = $this->baseURL . $indexPage; + $uri = new URI($tempUri); + + if ($configApp->forceGlobalSecureRequests) { + $uri->setScheme('https'); + } + + $parts = parse_url((string) $uri); + if ($parts === false) { + throw HTTPException::forUnableToParseURI($uri); + } + $this->applyParts($parts); + + $this->setPath('/'); + } + + /** + * Sets baseSegments. + */ + private function setBaseSeegments(): void + { + $basePath = (new URI($this->baseURL))->getPath(); + $this->baseSegments = $this->convertToSegments($basePath); + + if ($this->indexPage) { + $this->baseSegments[] = $this->indexPage; + } + } + + public function setURI(?string $uri = null) + { + throw new BadMethodCallException('Cannot use this method.'); + } + + /** + * Returns the URI path relative to baseURL. + * + * @return string The Route path. + */ + public function getRoutePath(): string + { + return $this->routePath; + } + + /** + * Returns the URI segments of the path as an array. + */ + public function getSegments(): array + { + return $this->segments; + } + + /** + * Returns the value of a specific segment of the URI path relative to baseURL. + * + * @param int $number Segment number + * @param string $default Default value + * + * @return string The value of the segment. If no segment is found, + * throws HTTPException + */ + public function getSegment(int $number, string $default = ''): string + { + if ($number < 1) { + throw HTTPException::forURISegmentOutOfRange($number); + } + + if ($number > count($this->segments) && ! $this->silent) { + throw HTTPException::forURISegmentOutOfRange($number); + } + + // The segment should treat the array as 1-based for the user + // but we still have to deal with a zero-based array. + $number--; + + return $this->segments[$number] ?? $default; + } + + /** + * Set the value of a specific segment of the URI path relative to baseURL. + * Allows to set only existing segments or add new one. + * + * @param int $number The segment number. Starting with 1. + * @param string $value The segment value. + * + * @return $this + */ + public function setSegment(int $number, $value) + { + if ($number < 1) { + throw HTTPException::forURISegmentOutOfRange($number); + } + + if ($number > count($this->segments) + 1) { + if ($this->silent) { + return $this; + } + + throw HTTPException::forURISegmentOutOfRange($number); + } + + // The segment should treat the array as 1-based for the user, + // but we still have to deal with a zero-based array. + $number--; + + $this->segments[$number] = $value; + + $this->refreshPath(); + + return $this; + } + + /** + * Returns the total number of segments. + */ + public function getTotalSegments(): int + { + return count($this->segments); + } + + /** + * Formats the URI as a string. + */ + public function __toString(): string + { + return static::createURIString( + $this->getScheme(), + $this->getAuthority(), + $this->getPath(), // Absolute URIs should use a "/" for an empty path + $this->getQuery(), + $this->getFragment() + ); + } + + /** + * Sets the route path (and segments). + * + * @return $this + */ + public function setPath(string $path) + { + $this->routePath = $this->filterPath($path); + + $this->segments = $this->convertToSegments($this->routePath); + + $this->refreshPath(); + + return $this; + } + + /** + * Converts path to segments + */ + private function convertToSegments(string $path): array + { + $tempPath = trim($path, '/'); + + return ($tempPath === '') ? [] : explode('/', $tempPath); + } + + /** + * Sets the path portion of the URI based on segments. + * + * @return $this + * + * @deprecated This method will be private. + */ + public function refreshPath() + { + $allSegments = array_merge($this->baseSegments, $this->segments); + $this->path = '/' . $this->filterPath(implode('/', $allSegments)); + + $this->routePath = $this->filterPath(implode('/', $this->segments)); + + if ($this->routePath === '') { + $this->routePath = '/'; + + if ($this->indexPage !== '') { + $this->path .= '/'; + } + } + + return $this; + } + + /** + * Saves our parts from a parse_url() call. + */ + protected function applyParts(array $parts) + { + if (! empty($parts['host'])) { + $this->host = $parts['host']; + } + if (! empty($parts['user'])) { + $this->user = $parts['user']; + } + if (isset($parts['path']) && $parts['path'] !== '') { + $this->path = $this->filterPath($parts['path']); + } + if (! empty($parts['query'])) { + $this->setQuery($parts['query']); + } + if (! empty($parts['fragment'])) { + $this->fragment = $parts['fragment']; + } + + // Scheme + if (isset($parts['scheme'])) { + $this->setScheme(rtrim($parts['scheme'], ':/')); + } else { + $this->setScheme('http'); + } + + // Port + if (isset($parts['port']) && $parts['port'] !== null) { + // Valid port numbers are enforced by earlier parse_url() or setPort() + $this->port = $parts['port']; + } + + if (isset($parts['pass'])) { + $this->password = $parts['pass']; + } + } +} diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 1e11953051bb..55c359d17d98 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -797,6 +797,8 @@ public function getBaseURL(): string * Sets the path portion of the URI based on segments. * * @return $this + * + * @deprecated This method will be private. */ public function refreshPath() { diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php new file mode 100644 index 000000000000..3a7c532519f6 --- /dev/null +++ b/tests/system/HTTP/SiteURITest.php @@ -0,0 +1,245 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use BadMethodCallException; +use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\Test\CIUnitTestCase; +use Config\App; + +/** + * @backupGlobals enabled + * + * @internal + * + * @group Others + */ +final class SiteURITest extends CIUnitTestCase +{ + public function testConstructor() + { + $config = new App(); + + $uri = new SiteURI($config); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://example.com/index.php/', (string) $uri); + $this->assertSame('/index.php/', $uri->getPath()); + } + + public function testConstructorSubfolder() + { + $config = new App(); + $config->baseURL = 'http://example.com/ci4/'; + + $uri = new SiteURI($config); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://example.com/ci4/index.php/', (string) $uri); + $this->assertSame('/ci4/index.php/', $uri->getPath()); + } + + public function testConstructorForceGlobalSecureRequests() + { + $config = new App(); + $config->forceGlobalSecureRequests = true; + + $uri = new SiteURI($config); + + $this->assertSame('https://example.com/index.php/', (string) $uri); + } + + public function testConstructorIndexPageEmpty() + { + $config = new App(); + $config->indexPage = ''; + + $uri = new SiteURI($config); + + $this->assertSame('http://example.com/', (string) $uri); + } + + public function testSetPath() + { + $config = new App(); + + $uri = new SiteURI($config); + + $uri->setPath('test/method'); + + $this->assertSame('http://example.com/index.php/test/method', (string) $uri); + $this->assertSame('test/method', $uri->getRoutePath()); + $this->assertSame('/index.php/test/method', $uri->getPath()); + $this->assertSame(['test', 'method'], $uri->getSegments()); + $this->assertSame('test', $uri->getSegment(1)); + $this->assertSame(2, $uri->getTotalSegments()); + } + + public function testSetPathSubfolder() + { + $config = new App(); + $config->baseURL = 'http://example.com/ci4/'; + + $uri = new SiteURI($config); + + $uri->setPath('test/method'); + + $this->assertSame('http://example.com/ci4/index.php/test/method', (string) $uri); + $this->assertSame('test/method', $uri->getRoutePath()); + $this->assertSame('/ci4/index.php/test/method', $uri->getPath()); + $this->assertSame(['test', 'method'], $uri->getSegments()); + $this->assertSame('test', $uri->getSegment(1)); + $this->assertSame(2, $uri->getTotalSegments()); + } + + public function testSetPathEmpty() + { + $config = new App(); + + $uri = new SiteURI($config); + + $uri->setPath(''); + + $this->assertSame('http://example.com/index.php/', (string) $uri); + $this->assertSame('/', $uri->getRoutePath()); + $this->assertSame('/index.php/', $uri->getPath()); + $this->assertSame([], $uri->getSegments()); + $this->assertSame(0, $uri->getTotalSegments()); + } + + public function testSetSegment() + { + $config = new App(); + + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $uri->setSegment(1, 'one'); + + $this->assertSame('http://example.com/index.php/one/method', (string) $uri); + $this->assertSame('one/method', $uri->getRoutePath()); + $this->assertSame('/index.php/one/method', $uri->getPath()); + $this->assertSame(['one', 'method'], $uri->getSegments()); + $this->assertSame('one', $uri->getSegment(1)); + $this->assertSame(2, $uri->getTotalSegments()); + } + + public function testSetSegmentOutOfRange() + { + $this->expectException(HTTPException::class); + + $config = new App(); + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $uri->setSegment(4, 'four'); + } + + public function testSetSegmentSilentOutOfRange() + { + $config = new App(); + $uri = new SiteURI($config); + $uri->setPath('one/method'); + $uri->setSilent(); + + $uri->setSegment(4, 'four'); + $this->assertSame(['one', 'method'], $uri->getSegments()); + } + + public function testSetSegmentZero() + { + $this->expectException(HTTPException::class); + + $config = new App(); + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $uri->setSegment(0, 'four'); + } + + public function testSetSegmentSubfolder() + { + $config = new App(); + $config->baseURL = 'http://example.com/ci4/'; + + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $uri->setSegment(1, 'one'); + + $this->assertSame('http://example.com/ci4/index.php/one/method', (string) $uri); + $this->assertSame('one/method', $uri->getRoutePath()); + $this->assertSame('/ci4/index.php/one/method', $uri->getPath()); + $this->assertSame(['one', 'method'], $uri->getSegments()); + $this->assertSame('one', $uri->getSegment(1)); + $this->assertSame(2, $uri->getTotalSegments()); + } + + public function testGetRoutePath() + { + $config = new App(); + $uri = new SiteURI($config); + + $this->assertSame('/', $uri->getRoutePath()); + } + + public function testGetSegments() + { + $config = new App(); + $uri = new SiteURI($config); + + $this->assertSame([], $uri->getSegments()); + } + + public function testGetSegmentZero() + { + $this->expectException(HTTPException::class); + + $config = new App(); + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $uri->getSegment(0); + } + + public function testGetSegmentOutOfRange() + { + $this->expectException(HTTPException::class); + + $config = new App(); + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $this->assertSame('method', $uri->getSegment(2)); + $this->assertSame('', $uri->getSegment(3)); + + $uri->getSegment(4); + } + + public function testGetTotalSegments() + { + $config = new App(); + $uri = new SiteURI($config); + + $this->assertSame(0, $uri->getTotalSegments()); + } + + public function testSetURI() + { + $this->expectException(BadMethodCallException::class); + + $config = new App(); + $uri = new SiteURI($config); + + $uri->setURI('http://another.site.example.jp/'); + } +} From 7eec7bb9e146a648e7fa16f2dbd8d26b1c84d466 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 14 Feb 2023 17:57:52 +0900 Subject: [PATCH 337/485] chore: add system/HTTP/SiteURI.php --- .github/workflows/test-phpcpd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-phpcpd.yml b/.github/workflows/test-phpcpd.yml index 31dcdb232749..f08945514386 100644 --- a/.github/workflows/test-phpcpd.yml +++ b/.github/workflows/test-phpcpd.yml @@ -55,4 +55,5 @@ jobs: --exclude system/Database/OCI8/Builder.php --exclude system/Database/Postgre/Builder.php --exclude system/Debug/Exceptions.php + --exclude system/HTTP/SiteURI.php -- app/ public/ system/ From 5593f8a481edc7f7e21950e996b270fd10637387 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 14 Feb 2023 18:48:16 +0900 Subject: [PATCH 338/485] docs: add @deprecated --- system/HTTP/URI.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 55c359d17d98..9b525e627fc6 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -34,11 +34,15 @@ class URI * Current URI string * * @var string + * + * @deprecated Not used. */ protected $uriString; /** * The Current baseURL. + * + * @deprecated Use SiteURI instead. */ private ?string $baseURL = null; @@ -773,6 +777,8 @@ public function setPath(string $path) * Sets the current baseURL. * * @interal + * + * @deprecated Use SiteURI instead. */ public function setBaseURL(string $baseURL): void { @@ -783,6 +789,8 @@ public function setBaseURL(string $baseURL): void * Returns the current baseURL. * * @interal + * + * @deprecated Use SiteURI instead. */ public function getBaseURL(): string { From 8a66de596999aa42b649bb48bbec173af068a50b Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 14 Feb 2023 21:14:31 +0900 Subject: [PATCH 339/485] test: fix incorrect test One test one assertion. --- tests/system/HTTP/SiteURITest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 3a7c532519f6..d92421e58830 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -219,10 +219,7 @@ public function testGetSegmentOutOfRange() $uri = new SiteURI($config); $uri->setPath('test/method'); - $this->assertSame('method', $uri->getSegment(2)); - $this->assertSame('', $uri->getSegment(3)); - - $uri->getSegment(4); + $uri->getSegment(3); } public function testGetTotalSegments() From 406128f65d535eaf387750d9ec37a27ad77ce0c5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 08:18:15 +0900 Subject: [PATCH 340/485] refactor: fix typo in method name --- system/HTTP/SiteURI.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index bf3e06903188..6de923e66307 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -78,7 +78,7 @@ public function __construct(App $configApp) $this->baseURL = $baseURL; $this->indexPage = $configApp->indexPage; - $this->setBaseSeegments(); + $this->setBaseSegments(); // Check for an index page $indexPage = ''; @@ -105,7 +105,7 @@ public function __construct(App $configApp) /** * Sets baseSegments. */ - private function setBaseSeegments(): void + private function setBaseSegments(): void { $basePath = (new URI($this->baseURL))->getPath(); $this->baseSegments = $this->convertToSegments($basePath); From 7d91c0e9bdb5791c0c718a765cae91058869a8c3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 08:41:37 +0900 Subject: [PATCH 341/485] docs: update PHPDocs --- system/HTTP/URI.php | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 9b525e627fc6..4f12b3464260 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -263,6 +263,8 @@ public function __construct(?string $uri = null) * If $silent == true, then will not throw exceptions and will * attempt to continue gracefully. * + * Note: Method not in PSR-7 + * * @return URI */ public function setSilent(bool $silent = true) @@ -276,6 +278,8 @@ public function setSilent(bool $silent = true) * If $raw == true, then will use parseStr() method * instead of native parse_str() function. * + * Note: Method not in PSR-7 + * * @return URI */ public function useRawQueryString(bool $raw = true) @@ -291,6 +295,8 @@ public function useRawQueryString(bool $raw = true) * @return URI * * @throws HTTPException + * + * @deprecated This method will be private. */ public function setURI(?string $uri = null) { @@ -408,6 +414,8 @@ public function getUserInfo() * Temporarily sets the URI to show a password in userInfo. Will * reset itself after the first call to authority(). * + * Note: Method not in PSR-7 + * * @return URI */ public function showPassword(bool $val = true) @@ -567,6 +575,8 @@ public function getSegment(int $number, string $default = ''): string * Set the value of a specific segment of the URI path. * Allows to set only existing segments or add new one. * + * Note: Method not in PSR-7 + * * @param int $number Segment number starting at 1 * @param int|string $value * @@ -598,6 +608,8 @@ public function setSegment(int $number, $value) /** * Returns the total number of segments. + * + * Note: Method not in PSR-7 */ public function getTotalSegments(): int { @@ -665,6 +677,8 @@ private function changeSchemeAndPath(string $scheme, string $path): array /** * Parses the given string and saves the appropriate authority pieces. * + * Note: Method not in PSR-7 + * * @return $this */ public function setAuthority(string $str) @@ -694,6 +708,8 @@ public function setAuthority(string $str) * @see https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml * * @return $this + * + * @TODO PSR-7: Should be `withScheme($scheme)`. */ public function setScheme(string $str) { @@ -710,6 +726,8 @@ public function setScheme(string $str) * @param string $pass The user's password * * @return $this + * + * @TODO PSR-7: Should be `withUserInfo($user, $password = null)`. */ public function setUserInfo(string $user, string $pass) { @@ -723,6 +741,8 @@ public function setUserInfo(string $user, string $pass) * Sets the host name to use. * * @return $this + * + * @TODO PSR-7: Should be `withHost($host)`. */ public function setHost(string $str) { @@ -737,6 +757,8 @@ public function setHost(string $str) * @param int $port * * @return $this + * + * @TODO PSR-7: Should be `withPort($port)`. */ public function setPort(?int $port = null) { @@ -761,6 +783,8 @@ public function setPort(?int $port = null) * Sets the path portion of the URI. * * @return $this + * + * @TODO PSR-7: Should be `withPath($port)`. */ public function setPath(string $path) { @@ -824,6 +848,8 @@ public function refreshPath() * to clean the various parts of the query keys and values. * * @return $this + * + * @TODO PSR-7: Should be `withQuery($query)`. */ public function setQuery(string $query) { @@ -854,6 +880,8 @@ public function setQuery(string $query) * portion of the URI. * * @return URI + * + * @TODO: PSR-7: Should be `withQueryParams(array $query)` */ public function setQueryArray(array $query) { @@ -865,7 +893,9 @@ public function setQueryArray(array $query) /** * Adds a single new element to the query vars. * - * @param int|string $value + * Note: Method not in PSR-7 + * + * @param int|string|null $value * * @return $this */ @@ -879,6 +909,8 @@ public function addQuery(string $key, $value = null) /** * Removes one or more query vars from the URI. * + * Note: Method not in PSR-7 + * * @param string ...$params * * @return $this @@ -896,6 +928,8 @@ public function stripQuery(...$params) * Filters the query variables so that only the keys passed in * are kept. The rest are removed from the object. * + * Note: Method not in PSR-7 + * * @param string ...$params * * @return $this @@ -923,6 +957,8 @@ public function keepQuery(...$params) * @see https://tools.ietf.org/html/rfc3986#section-3.5 * * @return $this + * + * @TODO PSR-7: Should be `withFragment($fragment)`. */ public function setFragment(string $string) { From b728948d91c3eb94a3e6511962f29969871524a0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 13:06:15 +0900 Subject: [PATCH 342/485] docs: add @deprecated --- system/HTTP/SiteURI.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 6de923e66307..4cad657ca1f3 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -115,6 +115,9 @@ private function setBaseSegments(): void } } + /** + * @deprecated + */ public function setURI(?string $uri = null) { throw new BadMethodCallException('Cannot use this method.'); From 9af4994bc3bd15ef3436cc9e17b5aaa8f219b8f2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 13:33:22 +0900 Subject: [PATCH 343/485] feat: add validation for baseURL --- system/HTTP/SiteURI.php | 8 ++++++++ tests/system/HTTP/SiteURITest.php | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 4cad657ca1f3..11299468fc1c 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -12,6 +12,7 @@ namespace CodeIgniter\HTTP; use BadMethodCallException; +use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\App; @@ -75,6 +76,13 @@ public function __construct(App $configApp) // baseURL, so let's help them out. $baseURL = rtrim($configApp->baseURL, '/ ') . '/'; + // Validate baseURL + if (filter_var($baseURL, FILTER_VALIDATE_URL) === false) { + throw new ConfigException( + 'Config\App::$baseURL is invalid.' + ); + } + $this->baseURL = $baseURL; $this->indexPage = $configApp->indexPage; diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index d92421e58830..833e52747ceb 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\HTTP; use BadMethodCallException; +use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\Test\CIUnitTestCase; use Config\App; @@ -68,6 +69,16 @@ public function testConstructorIndexPageEmpty() $this->assertSame('http://example.com/', (string) $uri); } + public function testConstructorInvalidBaseURL() + { + $this->expectException(ConfigException::class); + + $config = new App(); + $config->baseURL = 'invalid'; + + new SiteURI($config); + } + public function testSetPath() { $config = new App(); From 5fa53059ff93fa94cce56ea4272244c7f27472cd Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 13:33:56 +0900 Subject: [PATCH 344/485] feat: add getBaseURL() implementation --- system/HTTP/SiteURI.php | 10 ++++++++++ tests/system/HTTP/SiteURITest.php | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 11299468fc1c..4c5e66417158 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -131,6 +131,16 @@ public function setURI(?string $uri = null) throw new BadMethodCallException('Cannot use this method.'); } + /** + * Returns the baseURL. + * + * @interal + */ + public function getBaseURL(): string + { + return $this->baseURL; + } + /** * Returns the URI path relative to baseURL. * diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 833e52747ceb..5d0a3d134024 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -250,4 +250,12 @@ public function testSetURI() $uri->setURI('http://another.site.example.jp/'); } + + public function testGetBaseURL() + { + $config = new App(); + $uri = new SiteURI($config); + + $this->assertSame('http://example.com/', $uri->getBaseURL()); + } } From c8d5eefaf1514e88cd52abf10552681dd43b5bef Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 17 Feb 2023 08:32:43 +0900 Subject: [PATCH 345/485] feat: add param $relativePath to constructor Change the condition to add / after index.php. --- system/HTTP/SiteURI.php | 62 ++++++++++++++++++++++--------- tests/system/HTTP/SiteURITest.php | 33 ++++++++++++++++ 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 4c5e66417158..44f819ea8f43 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -70,20 +70,13 @@ class SiteURI extends URI */ private string $routePath; - public function __construct(App $configApp) + /** + * @param string $relativePath URI path relative to baseURL. May include + * queries or fragments. + */ + public function __construct(App $configApp, string $relativePath = '') { - // It's possible the user forgot a trailing slash on their - // baseURL, so let's help them out. - $baseURL = rtrim($configApp->baseURL, '/ ') . '/'; - - // Validate baseURL - if (filter_var($baseURL, FILTER_VALIDATE_URL) === false) { - throw new ConfigException( - 'Config\App::$baseURL is invalid.' - ); - } - - $this->baseURL = $baseURL; + $this->baseURL = $this->normalizeBaseURL($configApp); $this->indexPage = $configApp->indexPage; $this->setBaseSegments(); @@ -91,10 +84,17 @@ public function __construct(App $configApp) // Check for an index page $indexPage = ''; if ($configApp->indexPage !== '') { - $indexPage = $configApp->indexPage . '/'; + $indexPage = $configApp->indexPage; + + // Check if we need a separator + if ($relativePath !== '' && $relativePath[0] !== '/' && $relativePath[0] !== '?') { + $indexPage .= '/'; + } } - $tempUri = $this->baseURL . $indexPage; + $relativePath = URI::removeDotSegments($relativePath); + + $tempUri = $this->baseURL . $indexPage . $relativePath; $uri = new URI($tempUri); if ($configApp->forceGlobalSecureRequests) { @@ -107,7 +107,25 @@ public function __construct(App $configApp) } $this->applyParts($parts); - $this->setPath('/'); + $parts = explode('?', $relativePath); + $routePath = $parts[0]; + $this->setRoutePath($routePath); + } + + private function normalizeBaseURL(App $configApp): string + { + // It's possible the user forgot a trailing slash on their + // baseURL, so let's help them out. + $baseURL = rtrim($configApp->baseURL, '/ ') . '/'; + + // Validate baseURL + if (filter_var($baseURL, FILTER_VALIDATE_URL) === false) { + throw new ConfigException( + 'Config\App::$baseURL is invalid.' + ); + } + + return $baseURL; } /** @@ -247,14 +265,22 @@ public function __toString(): string * @return $this */ public function setPath(string $path) + { + $this->setRoutePath($path); + + return $this; + } + + /** + * Sets the route path (and segments). + */ + private function setRoutePath(string $path): void { $this->routePath = $this->filterPath($path); $this->segments = $this->convertToSegments($this->routePath); $this->refreshPath(); - - return $this; } /** diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 5d0a3d134024..74e035868196 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -37,6 +37,27 @@ public function testConstructor() $this->assertSame('/index.php/', $uri->getPath()); } + public function testConstructorRelativePath() + { + $config = new App(); + + $uri = new SiteURI($config, 'one/two'); + + $this->assertSame('http://example.com/index.php/one/two', (string) $uri); + $this->assertSame('/index.php/one/two', $uri->getPath()); + } + + public function testConstructorRelativePathWithQuery() + { + $config = new App(); + + $uri = new SiteURI($config, 'one/two?foo=1&bar=2'); + + $this->assertSame('http://example.com/index.php/one/two?foo=1&bar=2', (string) $uri); + $this->assertSame('/index.php/one/two', $uri->getPath()); + $this->assertSame('foo=1&bar=2', $uri->getQuery()); + } + public function testConstructorSubfolder() { $config = new App(); @@ -49,6 +70,18 @@ public function testConstructorSubfolder() $this->assertSame('/ci4/index.php/', $uri->getPath()); } + public function testConstructorSubfolderRelativePathWithQuery() + { + $config = new App(); + $config->baseURL = 'http://example.com/ci4/'; + + $uri = new SiteURI($config, 'one/two?foo=1&bar=2'); + + $this->assertSame('http://example.com/ci4/index.php/one/two?foo=1&bar=2', (string) $uri); + $this->assertSame('/ci4/index.php/one/two', $uri->getPath()); + $this->assertSame('foo=1&bar=2', $uri->getQuery()); + } + public function testConstructorForceGlobalSecureRequests() { $config = new App(); From b30035be7390acf50c366a3c7c2ded51c7299f38 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 17 Feb 2023 08:58:14 +0900 Subject: [PATCH 346/485] feat: add param $host to constructor --- system/HTTP/SiteURI.php | 16 +++++++++++++++- tests/system/HTTP/SiteURITest.php | 12 ++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 44f819ea8f43..61f9cbbe214d 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -73,8 +73,10 @@ class SiteURI extends URI /** * @param string $relativePath URI path relative to baseURL. May include * queries or fragments. + * @param string $host Hostname. If it is not in $allowedHostnames, + * just be ignored. */ - public function __construct(App $configApp, string $relativePath = '') + public function __construct(App $configApp, string $relativePath = '', string $host = '') { $this->baseURL = $this->normalizeBaseURL($configApp); $this->indexPage = $configApp->indexPage; @@ -97,21 +99,33 @@ public function __construct(App $configApp, string $relativePath = '') $tempUri = $this->baseURL . $indexPage . $relativePath; $uri = new URI($tempUri); + // Update scheme if ($configApp->forceGlobalSecureRequests) { $uri->setScheme('https'); } + // Update host + if ($host !== '' && $this->checkHost($host, $configApp->allowedHostnames)) { + $uri->setHost($host); + } + $parts = parse_url((string) $uri); if ($parts === false) { throw HTTPException::forUnableToParseURI($uri); } $this->applyParts($parts); + // Set routePath $parts = explode('?', $relativePath); $routePath = $parts[0]; $this->setRoutePath($routePath); } + private function checkHost(string $host, array $allowedHostnames): bool + { + return in_array($host, $allowedHostnames, true); + } + private function normalizeBaseURL(App $configApp): string { // It's possible the user forgot a trailing slash on their diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 74e035868196..413ab66ee57c 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -58,6 +58,18 @@ public function testConstructorRelativePathWithQuery() $this->assertSame('foo=1&bar=2', $uri->getQuery()); } + public function testConstructorHost() + { + $config = new App(); + $config->allowedHostnames = ['sub.example.com']; + + $uri = new SiteURI($config, '', 'sub.example.com'); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://sub.example.com/index.php/', (string) $uri); + $this->assertSame('/index.php/', $uri->getPath()); + } + public function testConstructorSubfolder() { $config = new App(); From 9d823382ac66d00598417b495bb17150c1b967af Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 17 Feb 2023 09:06:22 +0900 Subject: [PATCH 347/485] feat: add param $scheme to constructor --- system/HTTP/SiteURI.php | 10 +++++++--- tests/system/HTTP/SiteURITest.php | 10 ++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 61f9cbbe214d..ea2bab91078f 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -73,10 +73,12 @@ class SiteURI extends URI /** * @param string $relativePath URI path relative to baseURL. May include * queries or fragments. - * @param string $host Hostname. If it is not in $allowedHostnames, + * @param string $host Optional hostname. If it is not in $allowedHostnames, * just be ignored. + * @param string $scheme Optional scheme. 'http' or 'https'. + * @phpstan-param 'http'|'https'|'' $scheme */ - public function __construct(App $configApp, string $relativePath = '', string $host = '') + public function __construct(App $configApp, string $relativePath = '', string $host = '', string $scheme = '') { $this->baseURL = $this->normalizeBaseURL($configApp); $this->indexPage = $configApp->indexPage; @@ -100,7 +102,9 @@ public function __construct(App $configApp, string $relativePath = '', string $h $uri = new URI($tempUri); // Update scheme - if ($configApp->forceGlobalSecureRequests) { + if ($scheme !== '') { + $uri->setScheme($scheme); + } elseif ($configApp->forceGlobalSecureRequests) { $uri->setScheme('https'); } diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 413ab66ee57c..bdc6679bacc1 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -70,6 +70,16 @@ public function testConstructorHost() $this->assertSame('/index.php/', $uri->getPath()); } + public function testConstructorScheme() + { + $config = new App(); + + $uri = new SiteURI($config, '', '', 'https'); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('https://example.com/index.php/', (string) $uri); + } + public function testConstructorSubfolder() { $config = new App(); From b231d1c2838fb37f1ba4bc00cecdaf3877585f0c Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 17 Feb 2023 09:11:23 +0900 Subject: [PATCH 348/485] style: break long line --- system/HTTP/SiteURI.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index ea2bab91078f..6b07bca62aa1 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -78,8 +78,12 @@ class SiteURI extends URI * @param string $scheme Optional scheme. 'http' or 'https'. * @phpstan-param 'http'|'https'|'' $scheme */ - public function __construct(App $configApp, string $relativePath = '', string $host = '', string $scheme = '') - { + public function __construct( + App $configApp, + string $relativePath = '', + string $host = '', + string $scheme = '' + ) { $this->baseURL = $this->normalizeBaseURL($configApp); $this->indexPage = $configApp->indexPage; From 4932572a5dcd8d968c10c2412799db232f9d512d Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 17 Feb 2023 10:01:03 +0900 Subject: [PATCH 349/485] fix: remove unneeded methods getSegment() accepts n+1. --- system/HTTP/SiteURI.php | 64 +++++-------------------------- tests/system/HTTP/SiteURITest.php | 2 +- 2 files changed, 10 insertions(+), 56 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 6b07bca62aa1..6a2d7f594af0 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -192,21 +192,17 @@ public function getRoutePath(): string } /** - * Returns the URI segments of the path as an array. - */ - public function getSegments(): array - { - return $this->segments; - } - - /** - * Returns the value of a specific segment of the URI path relative to baseURL. + * Returns the value of a specific segment of the URI path. + * Allows to get only existing segments or the next one. * - * @param int $number Segment number + * @param int $number Segment number starting at 1 * @param string $default Default value * - * @return string The value of the segment. If no segment is found, - * throws HTTPException + * @return string The value of the segment. If you specify the last +1 + * segment, the $default value. If you specify the last +2 + * or more throws HTTPException. + * + * @TODO remove this method after merging #7267 */ public function getSegment(int $number, string $default = ''): string { @@ -214,7 +210,7 @@ public function getSegment(int $number, string $default = ''): string throw HTTPException::forURISegmentOutOfRange($number); } - if ($number > count($this->segments) && ! $this->silent) { + if ($number > count($this->segments) + 1 && ! $this->silent) { throw HTTPException::forURISegmentOutOfRange($number); } @@ -225,48 +221,6 @@ public function getSegment(int $number, string $default = ''): string return $this->segments[$number] ?? $default; } - /** - * Set the value of a specific segment of the URI path relative to baseURL. - * Allows to set only existing segments or add new one. - * - * @param int $number The segment number. Starting with 1. - * @param string $value The segment value. - * - * @return $this - */ - public function setSegment(int $number, $value) - { - if ($number < 1) { - throw HTTPException::forURISegmentOutOfRange($number); - } - - if ($number > count($this->segments) + 1) { - if ($this->silent) { - return $this; - } - - throw HTTPException::forURISegmentOutOfRange($number); - } - - // The segment should treat the array as 1-based for the user, - // but we still have to deal with a zero-based array. - $number--; - - $this->segments[$number] = $value; - - $this->refreshPath(); - - return $this; - } - - /** - * Returns the total number of segments. - */ - public function getTotalSegments(): int - { - return count($this->segments); - } - /** * Formats the URI as a string. */ diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index bdc6679bacc1..e670e023b9f4 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -285,7 +285,7 @@ public function testGetSegmentOutOfRange() $uri = new SiteURI($config); $uri->setPath('test/method'); - $uri->getSegment(3); + $uri->getSegment(4); } public function testGetTotalSegments() From c3628d14c63a4507927b19fab5327353ddf11f2a Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 17 Feb 2023 15:24:36 +0900 Subject: [PATCH 350/485] fix: change default values to null For consistency with previous implementations. --- system/HTTP/SiteURI.php | 20 ++++++++++---------- tests/system/HTTP/SiteURITest.php | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 6a2d7f594af0..110d189e9d12 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -71,18 +71,18 @@ class SiteURI extends URI private string $routePath; /** - * @param string $relativePath URI path relative to baseURL. May include - * queries or fragments. - * @param string $host Optional hostname. If it is not in $allowedHostnames, - * just be ignored. - * @param string $scheme Optional scheme. 'http' or 'https'. - * @phpstan-param 'http'|'https'|'' $scheme + * @param string $relativePath URI path relative to baseURL. May include + * queries or fragments. + * @param string|null $host Optional hostname. If it is not in + * $allowedHostnames, just be ignored. + * @param string|null $scheme Optional scheme. 'http' or 'https'. + * @phpstan-param 'http'|'https'|null $scheme */ public function __construct( App $configApp, string $relativePath = '', - string $host = '', - string $scheme = '' + ?string $host = null, + ?string $scheme = null ) { $this->baseURL = $this->normalizeBaseURL($configApp); $this->indexPage = $configApp->indexPage; @@ -106,14 +106,14 @@ public function __construct( $uri = new URI($tempUri); // Update scheme - if ($scheme !== '') { + if ($scheme !== null) { $uri->setScheme($scheme); } elseif ($configApp->forceGlobalSecureRequests) { $uri->setScheme('https'); } // Update host - if ($host !== '' && $this->checkHost($host, $configApp->allowedHostnames)) { + if ($host !== null && $this->checkHost($host, $configApp->allowedHostnames)) { $uri->setHost($host); } diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index e670e023b9f4..11153fb9b7fd 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -74,7 +74,7 @@ public function testConstructorScheme() { $config = new App(); - $uri = new SiteURI($config, '', '', 'https'); + $uri = new SiteURI($config, '', null, 'https'); $this->assertInstanceOf(SiteURI::class, $uri); $this->assertSame('https://example.com/index.php/', (string) $uri); From 57afaf9920465562e17e0a06b795bfde11dd364f Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 17 Feb 2023 18:23:36 +0900 Subject: [PATCH 351/485] fix: handling fragment --- system/HTTP/SiteURI.php | 1 + tests/system/HTTP/SiteURITest.php | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 110d189e9d12..958c503a5cdd 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -125,6 +125,7 @@ public function __construct( // Set routePath $parts = explode('?', $relativePath); + $parts = explode('#', $parts[0]); $routePath = $parts[0]; $this->setRoutePath($routePath); } diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 11153fb9b7fd..e8c2f4365b9c 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -58,6 +58,30 @@ public function testConstructorRelativePathWithQuery() $this->assertSame('foo=1&bar=2', $uri->getQuery()); } + public function testConstructorRelativePathWithFragment() + { + $config = new App(); + + $uri = new SiteURI($config, 'one/two#sec1'); + + $this->assertSame('http://example.com/index.php/one/two#sec1', (string) $uri); + $this->assertSame('/index.php/one/two', $uri->getPath()); + $this->assertSame('', $uri->getQuery()); + $this->assertSame('sec1', $uri->getFragment()); + } + + public function testConstructorRelativePathWithQueryAndFragment() + { + $config = new App(); + + $uri = new SiteURI($config, 'one/two?foo=1&bar=2#sec1'); + + $this->assertSame('http://example.com/index.php/one/two?foo=1&bar=2#sec1', (string) $uri); + $this->assertSame('/index.php/one/two', $uri->getPath()); + $this->assertSame('foo=1&bar=2', $uri->getQuery()); + $this->assertSame('sec1', $uri->getFragment()); + } + public function testConstructorHost() { $config = new App(); From 246eea249a526d562f30f45778eaff9e8575fc40 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 18 Feb 2023 13:13:34 +0900 Subject: [PATCH 352/485] fix: host in baseURL may be wrong --- system/HTTP/SiteURI.php | 32 +++++++++++++++++++++++-------- tests/system/HTTP/SiteURITest.php | 6 ++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 958c503a5cdd..84488a307bca 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -22,10 +22,18 @@ class SiteURI extends URI { /** - * The baseURL. + * The current baseURL. */ private string $baseURL; + /** + * The path part of baseURL. + * + * The baseURL "http://example.com/" → '/' + * The baseURL "http://localhost:8888/ci431/public/" → '/ci431/public/' + */ + private string $basePathWithoutIndexPage; + /** * The Index File. */ @@ -84,10 +92,10 @@ public function __construct( ?string $host = null, ?string $scheme = null ) { - $this->baseURL = $this->normalizeBaseURL($configApp); + $baseURL = $this->normalizeBaseURL($configApp); $this->indexPage = $configApp->indexPage; - $this->setBaseSegments(); + $this->setBasePath($baseURL); // Check for an index page $indexPage = ''; @@ -102,7 +110,7 @@ public function __construct( $relativePath = URI::removeDotSegments($relativePath); - $tempUri = $this->baseURL . $indexPage . $relativePath; + $tempUri = $baseURL . $indexPage . $relativePath; $uri = new URI($tempUri); // Update scheme @@ -128,6 +136,13 @@ public function __construct( $parts = explode('#', $parts[0]); $routePath = $parts[0]; $this->setRoutePath($routePath); + + // Set baseURL + $this->baseURL = URI::createURIString( + $this->getScheme(), + $this->getAuthority(), + $this->basePathWithoutIndexPage, + ); } private function checkHost(string $host, array $allowedHostnames): bool @@ -152,12 +167,13 @@ private function normalizeBaseURL(App $configApp): string } /** - * Sets baseSegments. + * Sets basePathWithoutIndexPage and baseSegments. */ - private function setBaseSegments(): void + private function setBasePath(string $baseURL): void { - $basePath = (new URI($this->baseURL))->getPath(); - $this->baseSegments = $this->convertToSegments($basePath); + $this->basePathWithoutIndexPage = (new URI($baseURL))->getPath(); + + $this->baseSegments = $this->convertToSegments($this->basePathWithoutIndexPage); if ($this->indexPage) { $this->baseSegments[] = $this->indexPage; diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index e8c2f4365b9c..9a8caa3a7d95 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -35,6 +35,7 @@ public function testConstructor() $this->assertInstanceOf(SiteURI::class, $uri); $this->assertSame('http://example.com/index.php/', (string) $uri); $this->assertSame('/index.php/', $uri->getPath()); + $this->assertSame('http://example.com/', $uri->getBaseURL()); } public function testConstructorRelativePath() @@ -92,6 +93,7 @@ public function testConstructorHost() $this->assertInstanceOf(SiteURI::class, $uri); $this->assertSame('http://sub.example.com/index.php/', (string) $uri); $this->assertSame('/index.php/', $uri->getPath()); + $this->assertSame('http://sub.example.com/', $uri->getBaseURL()); } public function testConstructorScheme() @@ -102,6 +104,7 @@ public function testConstructorScheme() $this->assertInstanceOf(SiteURI::class, $uri); $this->assertSame('https://example.com/index.php/', (string) $uri); + $this->assertSame('https://example.com/', $uri->getBaseURL()); } public function testConstructorSubfolder() @@ -114,6 +117,7 @@ public function testConstructorSubfolder() $this->assertInstanceOf(SiteURI::class, $uri); $this->assertSame('http://example.com/ci4/index.php/', (string) $uri); $this->assertSame('/ci4/index.php/', $uri->getPath()); + $this->assertSame('http://example.com/ci4/', $uri->getBaseURL()); } public function testConstructorSubfolderRelativePathWithQuery() @@ -136,6 +140,7 @@ public function testConstructorForceGlobalSecureRequests() $uri = new SiteURI($config); $this->assertSame('https://example.com/index.php/', (string) $uri); + $this->assertSame('https://example.com/', $uri->getBaseURL()); } public function testConstructorIndexPageEmpty() @@ -146,6 +151,7 @@ public function testConstructorIndexPageEmpty() $uri = new SiteURI($config); $this->assertSame('http://example.com/', (string) $uri); + $this->assertSame('http://example.com/', $uri->getBaseURL()); } public function testConstructorInvalidBaseURL() From 08bbc18006b22911c1b6d5c19de851d660ef3295 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 18 Feb 2023 13:14:54 +0900 Subject: [PATCH 353/485] fix: disable setBaseURL() method --- system/HTTP/SiteURI.php | 8 ++++++++ tests/system/HTTP/SiteURITest.php | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 84488a307bca..9f5cbb56ba5a 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -180,6 +180,14 @@ private function setBasePath(string $baseURL): void } } + /** + * @deprecated + */ + public function setBaseURL(string $baseURL): void + { + throw new BadMethodCallException('Cannot use this method.'); + } + /** * @deprecated */ diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 9a8caa3a7d95..578074ef5d6f 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -336,6 +336,16 @@ public function testSetURI() $uri->setURI('http://another.site.example.jp/'); } + public function testSetBaseURI() + { + $this->expectException(BadMethodCallException::class); + + $config = new App(); + $uri = new SiteURI($config); + + $uri->setBaseURL('http://another.site.example.jp/'); + } + public function testGetBaseURL() { $config = new App(); From ab6da6186727d74fdcaa261a178c21d3a170a379 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 20 Feb 2023 16:04:14 +0900 Subject: [PATCH 354/485] test: add assertions --- tests/system/HTTP/SiteURITest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 578074ef5d6f..807ac03f6ced 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -34,6 +34,7 @@ public function testConstructor() $this->assertInstanceOf(SiteURI::class, $uri); $this->assertSame('http://example.com/index.php/', (string) $uri); + $this->assertSame('/', $uri->getRoutePath()); $this->assertSame('/index.php/', $uri->getPath()); $this->assertSame('http://example.com/', $uri->getBaseURL()); } @@ -45,6 +46,7 @@ public function testConstructorRelativePath() $uri = new SiteURI($config, 'one/two'); $this->assertSame('http://example.com/index.php/one/two', (string) $uri); + $this->assertSame('one/two', $uri->getRoutePath()); $this->assertSame('/index.php/one/two', $uri->getPath()); } @@ -55,6 +57,7 @@ public function testConstructorRelativePathWithQuery() $uri = new SiteURI($config, 'one/two?foo=1&bar=2'); $this->assertSame('http://example.com/index.php/one/two?foo=1&bar=2', (string) $uri); + $this->assertSame('one/two', $uri->getRoutePath()); $this->assertSame('/index.php/one/two', $uri->getPath()); $this->assertSame('foo=1&bar=2', $uri->getQuery()); } @@ -66,6 +69,7 @@ public function testConstructorRelativePathWithFragment() $uri = new SiteURI($config, 'one/two#sec1'); $this->assertSame('http://example.com/index.php/one/two#sec1', (string) $uri); + $this->assertSame('one/two', $uri->getRoutePath()); $this->assertSame('/index.php/one/two', $uri->getPath()); $this->assertSame('', $uri->getQuery()); $this->assertSame('sec1', $uri->getFragment()); @@ -78,6 +82,7 @@ public function testConstructorRelativePathWithQueryAndFragment() $uri = new SiteURI($config, 'one/two?foo=1&bar=2#sec1'); $this->assertSame('http://example.com/index.php/one/two?foo=1&bar=2#sec1', (string) $uri); + $this->assertSame('one/two', $uri->getRoutePath()); $this->assertSame('/index.php/one/two', $uri->getPath()); $this->assertSame('foo=1&bar=2', $uri->getQuery()); $this->assertSame('sec1', $uri->getFragment()); @@ -92,6 +97,7 @@ public function testConstructorHost() $this->assertInstanceOf(SiteURI::class, $uri); $this->assertSame('http://sub.example.com/index.php/', (string) $uri); + $this->assertSame('/', $uri->getRoutePath()); $this->assertSame('/index.php/', $uri->getPath()); $this->assertSame('http://sub.example.com/', $uri->getBaseURL()); } @@ -116,6 +122,7 @@ public function testConstructorSubfolder() $this->assertInstanceOf(SiteURI::class, $uri); $this->assertSame('http://example.com/ci4/index.php/', (string) $uri); + $this->assertSame('/', $uri->getRoutePath()); $this->assertSame('/ci4/index.php/', $uri->getPath()); $this->assertSame('http://example.com/ci4/', $uri->getBaseURL()); } @@ -128,6 +135,7 @@ public function testConstructorSubfolderRelativePathWithQuery() $uri = new SiteURI($config, 'one/two?foo=1&bar=2'); $this->assertSame('http://example.com/ci4/index.php/one/two?foo=1&bar=2', (string) $uri); + $this->assertSame('one/two', $uri->getRoutePath()); $this->assertSame('/ci4/index.php/one/two', $uri->getPath()); $this->assertSame('foo=1&bar=2', $uri->getQuery()); } @@ -152,6 +160,8 @@ public function testConstructorIndexPageEmpty() $this->assertSame('http://example.com/', (string) $uri); $this->assertSame('http://example.com/', $uri->getBaseURL()); + $this->assertSame('/', $uri->getRoutePath()); + $this->assertSame('/', $uri->getPath()); } public function testConstructorInvalidBaseURL() From 82a567d09c3ba12f9bcddb8e1743aa100aec8c01 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 20 Feb 2023 16:44:14 +0900 Subject: [PATCH 355/485] refactor: SiteURI::__construct() --- system/HTTP/SiteURI.php | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 9f5cbb56ba5a..38e28763743e 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -92,24 +92,23 @@ public function __construct( ?string $host = null, ?string $scheme = null ) { - $baseURL = $this->normalizeBaseURL($configApp); $this->indexPage = $configApp->indexPage; + $baseURL = $this->normalizeBaseURL($configApp); $this->setBasePath($baseURL); + $relativePath = URI::removeDotSegments($relativePath); + // Remove starting slash + if ($relativePath !== '' && $relativePath[0] === '/') { + $relativePath = ltrim($relativePath, '/'); + } + // Check for an index page $indexPage = ''; if ($configApp->indexPage !== '') { - $indexPage = $configApp->indexPage; - - // Check if we need a separator - if ($relativePath !== '' && $relativePath[0] !== '/' && $relativePath[0] !== '?') { - $indexPage .= '/'; - } + $indexPage = $configApp->indexPage . '/'; } - $relativePath = URI::removeDotSegments($relativePath); - $tempUri = $baseURL . $indexPage . $relativePath; $uri = new URI($tempUri); @@ -254,7 +253,7 @@ public function __toString(): string return static::createURIString( $this->getScheme(), $this->getAuthority(), - $this->getPath(), // Absolute URIs should use a "/" for an empty path + $this->getPath(), $this->getQuery(), $this->getFragment() ); From ccfd79db821dcf721d817ea92ce2189b43e768fe Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 20 Feb 2023 16:46:29 +0900 Subject: [PATCH 356/485] test: add test cases for path starting with slash --- tests/system/HTTP/SiteURITest.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 807ac03f6ced..e00c65b0c89e 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -50,6 +50,17 @@ public function testConstructorRelativePath() $this->assertSame('/index.php/one/two', $uri->getPath()); } + public function testConstructorRelativePathStartWithSlash() + { + $config = new App(); + + $uri = new SiteURI($config, '/one/two'); + + $this->assertSame('http://example.com/index.php/one/two', (string) $uri); + $this->assertSame('one/two', $uri->getRoutePath()); + $this->assertSame('/index.php/one/two', $uri->getPath()); + } + public function testConstructorRelativePathWithQuery() { $config = new App(); @@ -190,6 +201,22 @@ public function testSetPath() $this->assertSame(2, $uri->getTotalSegments()); } + public function testSetPathStartWithSlash() + { + $config = new App(); + + $uri = new SiteURI($config); + + $uri->setPath('/test/method'); + + $this->assertSame('http://example.com/index.php/test/method', (string) $uri); + $this->assertSame('test/method', $uri->getRoutePath()); + $this->assertSame('/index.php/test/method', $uri->getPath()); + $this->assertSame(['test', 'method'], $uri->getSegments()); + $this->assertSame('test', $uri->getSegment(1)); + $this->assertSame(2, $uri->getTotalSegments()); + } + public function testSetPathSubfolder() { $config = new App(); From e733417b491a06057fec4b535e438ba5828485d7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 21 Feb 2023 13:38:53 +0900 Subject: [PATCH 357/485] fix: change behavior The $routePath never starts with `/`. If the path is `/`, the URI retain the trailing `/`. --- system/HTTP/SiteURI.php | 31 +++++++++------ tests/system/HTTP/SiteURITest.php | 65 +++++++++++++++++++++++++------ 2 files changed, 72 insertions(+), 24 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 38e28763743e..e444dbb7b66a 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -75,6 +75,8 @@ class SiteURI extends URI * * If the baseURL contains sub folders, this value will be different from * the current URI path. + * + * This value never starts with '/'. */ private string $routePath; @@ -98,18 +100,27 @@ public function __construct( $this->setBasePath($baseURL); $relativePath = URI::removeDotSegments($relativePath); - // Remove starting slash - if ($relativePath !== '' && $relativePath[0] === '/') { + // Remove starting slash unless it is `/`. + if ($relativePath !== '' && $relativePath[0] === '/' && $relativePath !== '/') { $relativePath = ltrim($relativePath, '/'); } + $tempPath = $relativePath; + // Check for an index page $indexPage = ''; if ($configApp->indexPage !== '') { - $indexPage = $configApp->indexPage . '/'; + $indexPage = $configApp->indexPage; + + // Check if we need a separator + if ($relativePath !== '' && $relativePath[0] !== '/' && $relativePath[0] !== '?') { + $indexPage .= '/'; + } + } elseif ($relativePath === '/') { + $tempPath = ''; } - $tempUri = $baseURL . $indexPage . $relativePath; + $tempUri = $baseURL . $indexPage . $tempPath; $uri = new URI($tempUri); // Update scheme @@ -305,16 +316,12 @@ public function refreshPath() $allSegments = array_merge($this->baseSegments, $this->segments); $this->path = '/' . $this->filterPath(implode('/', $allSegments)); - $this->routePath = $this->filterPath(implode('/', $this->segments)); - - if ($this->routePath === '') { - $this->routePath = '/'; - - if ($this->indexPage !== '') { - $this->path .= '/'; - } + if ($this->routePath === '/' && $this->path !== '/') { + $this->path .= '/'; } + $this->routePath = $this->filterPath(implode('/', $this->segments)); + return $this; } diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index e00c65b0c89e..0ca227616a2d 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -32,9 +32,22 @@ public function testConstructor() $uri = new SiteURI($config); + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://example.com/index.php', (string) $uri); + $this->assertSame('', $uri->getRoutePath()); + $this->assertSame('/index.php', $uri->getPath()); + $this->assertSame('http://example.com/', $uri->getBaseURL()); + } + + public function testConstructorPathSlash() + { + $config = new App(); + + $uri = new SiteURI($config, '/'); + $this->assertInstanceOf(SiteURI::class, $uri); $this->assertSame('http://example.com/index.php/', (string) $uri); - $this->assertSame('/', $uri->getRoutePath()); + $this->assertSame('', $uri->getRoutePath()); $this->assertSame('/index.php/', $uri->getPath()); $this->assertSame('http://example.com/', $uri->getBaseURL()); } @@ -107,9 +120,9 @@ public function testConstructorHost() $uri = new SiteURI($config, '', 'sub.example.com'); $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('http://sub.example.com/index.php/', (string) $uri); - $this->assertSame('/', $uri->getRoutePath()); - $this->assertSame('/index.php/', $uri->getPath()); + $this->assertSame('http://sub.example.com/index.php', (string) $uri); + $this->assertSame('', $uri->getRoutePath()); + $this->assertSame('/index.php', $uri->getPath()); $this->assertSame('http://sub.example.com/', $uri->getBaseURL()); } @@ -120,7 +133,7 @@ public function testConstructorScheme() $uri = new SiteURI($config, '', null, 'https'); $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('https://example.com/index.php/', (string) $uri); + $this->assertSame('https://example.com/index.php', (string) $uri); $this->assertSame('https://example.com/', $uri->getBaseURL()); } @@ -132,9 +145,9 @@ public function testConstructorSubfolder() $uri = new SiteURI($config); $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('http://example.com/ci4/index.php/', (string) $uri); - $this->assertSame('/', $uri->getRoutePath()); - $this->assertSame('/ci4/index.php/', $uri->getPath()); + $this->assertSame('http://example.com/ci4/index.php', (string) $uri); + $this->assertSame('', $uri->getRoutePath()); + $this->assertSame('/ci4/index.php', $uri->getPath()); $this->assertSame('http://example.com/ci4/', $uri->getBaseURL()); } @@ -158,7 +171,7 @@ public function testConstructorForceGlobalSecureRequests() $uri = new SiteURI($config); - $this->assertSame('https://example.com/index.php/', (string) $uri); + $this->assertSame('https://example.com/index.php', (string) $uri); $this->assertSame('https://example.com/', $uri->getBaseURL()); } @@ -171,7 +184,20 @@ public function testConstructorIndexPageEmpty() $this->assertSame('http://example.com/', (string) $uri); $this->assertSame('http://example.com/', $uri->getBaseURL()); - $this->assertSame('/', $uri->getRoutePath()); + $this->assertSame('', $uri->getRoutePath()); + $this->assertSame('/', $uri->getPath()); + } + + public function testConstructorIndexPageEmptyWithPathSlash() + { + $config = new App(); + $config->indexPage = ''; + + $uri = new SiteURI($config, '/'); + + $this->assertSame('http://example.com/', (string) $uri); + $this->assertSame('http://example.com/', $uri->getBaseURL()); + $this->assertSame('', $uri->getRoutePath()); $this->assertSame('/', $uri->getPath()); } @@ -242,8 +268,23 @@ public function testSetPathEmpty() $uri->setPath(''); + $this->assertSame('http://example.com/index.php', (string) $uri); + $this->assertSame('', $uri->getRoutePath()); + $this->assertSame('/index.php', $uri->getPath()); + $this->assertSame([], $uri->getSegments()); + $this->assertSame(0, $uri->getTotalSegments()); + } + + public function testSetPathSlash() + { + $config = new App(); + + $uri = new SiteURI($config); + + $uri->setPath('/'); + $this->assertSame('http://example.com/index.php/', (string) $uri); - $this->assertSame('/', $uri->getRoutePath()); + $this->assertSame('', $uri->getRoutePath()); $this->assertSame('/index.php/', $uri->getPath()); $this->assertSame([], $uri->getSegments()); $this->assertSame(0, $uri->getTotalSegments()); @@ -322,7 +363,7 @@ public function testGetRoutePath() $config = new App(); $uri = new SiteURI($config); - $this->assertSame('/', $uri->getRoutePath()); + $this->assertSame('', $uri->getRoutePath()); } public function testGetSegments() From 33fc02864f998ab5471bc891bb4756861727fb79 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 21 Feb 2023 14:50:31 +0900 Subject: [PATCH 358/485] test: use dataProviders --- tests/system/HTTP/SiteURITest.php | 424 +++++++++++++++--------------- 1 file changed, 219 insertions(+), 205 deletions(-) diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 0ca227616a2d..f7595063bd5e 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -26,90 +26,202 @@ */ final class SiteURITest extends CIUnitTestCase { - public function testConstructor() - { - $config = new App(); - - $uri = new SiteURI($config); - - $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('http://example.com/index.php', (string) $uri); - $this->assertSame('', $uri->getRoutePath()); - $this->assertSame('/index.php', $uri->getPath()); - $this->assertSame('http://example.com/', $uri->getBaseURL()); - } - - public function testConstructorPathSlash() - { - $config = new App(); + /** + * @dataProvider provideConstructor + */ + public function testConstructor( + string $baseURL, + string $indexPage, + string $relativePath, + string $expectedURI, + string $expectedRoutePath, + string $expectedPath, + string $expectedQuery, + string $expectedFragment, + array $expectedSegments, + int $expectedTotalSegments + ) { + $config = new App(); + $config->indexPage = $indexPage; + $config->baseURL = $baseURL; - $uri = new SiteURI($config, '/'); + $uri = new SiteURI($config, $relativePath); $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('http://example.com/index.php/', (string) $uri); - $this->assertSame('', $uri->getRoutePath()); - $this->assertSame('/index.php/', $uri->getPath()); - $this->assertSame('http://example.com/', $uri->getBaseURL()); - } - - public function testConstructorRelativePath() - { - $config = new App(); - - $uri = new SiteURI($config, 'one/two'); - - $this->assertSame('http://example.com/index.php/one/two', (string) $uri); - $this->assertSame('one/two', $uri->getRoutePath()); - $this->assertSame('/index.php/one/two', $uri->getPath()); - } - - public function testConstructorRelativePathStartWithSlash() - { - $config = new App(); - $uri = new SiteURI($config, '/one/two'); - - $this->assertSame('http://example.com/index.php/one/two', (string) $uri); - $this->assertSame('one/two', $uri->getRoutePath()); - $this->assertSame('/index.php/one/two', $uri->getPath()); - } - - public function testConstructorRelativePathWithQuery() - { - $config = new App(); - - $uri = new SiteURI($config, 'one/two?foo=1&bar=2'); - - $this->assertSame('http://example.com/index.php/one/two?foo=1&bar=2', (string) $uri); - $this->assertSame('one/two', $uri->getRoutePath()); - $this->assertSame('/index.php/one/two', $uri->getPath()); - $this->assertSame('foo=1&bar=2', $uri->getQuery()); - } - - public function testConstructorRelativePathWithFragment() - { - $config = new App(); - - $uri = new SiteURI($config, 'one/two#sec1'); - - $this->assertSame('http://example.com/index.php/one/two#sec1', (string) $uri); - $this->assertSame('one/two', $uri->getRoutePath()); - $this->assertSame('/index.php/one/two', $uri->getPath()); - $this->assertSame('', $uri->getQuery()); - $this->assertSame('sec1', $uri->getFragment()); - } - - public function testConstructorRelativePathWithQueryAndFragment() - { - $config = new App(); - - $uri = new SiteURI($config, 'one/two?foo=1&bar=2#sec1'); - - $this->assertSame('http://example.com/index.php/one/two?foo=1&bar=2#sec1', (string) $uri); - $this->assertSame('one/two', $uri->getRoutePath()); - $this->assertSame('/index.php/one/two', $uri->getPath()); - $this->assertSame('foo=1&bar=2', $uri->getQuery()); - $this->assertSame('sec1', $uri->getFragment()); + $this->assertSame($expectedURI, (string) $uri); + $this->assertSame($expectedRoutePath, $uri->getRoutePath()); + $this->assertSame($expectedPath, $uri->getPath()); + $this->assertSame($expectedQuery, $uri->getQuery()); + $this->assertSame($expectedFragment, $uri->getFragment()); + $this->assertSame($baseURL, $uri->getBaseURL()); + + $this->assertSame($expectedSegments, $uri->getSegments()); + $this->assertSame($expectedTotalSegments, $uri->getTotalSegments()); + } + + public function provideConstructor() + { + return array_merge($this->provideURIs(), $this->provideRelativePathWithQueryOrFragment()); + } + + public function provideURIs() + { + return [ + // $baseURL, $indexPage, $relativePath, $expectedURI, $expectedRoutePath, + // $expectedPath, $expectedQuery, $expectedFragment + '' => [ + 'http://example.com/', + 'index.php', + '', + 'http://example.com/index.php', + '', + '/index.php', + '', + '', + [], + 0, + ], + '/' => [ + 'http://example.com/', + 'index.php', + '/', + 'http://example.com/index.php/', + '', + '/index.php/', + '', + '', + [], + 0, + ], + 'one/two' => [ + 'http://example.com/', + 'index.php', + 'one/two', + 'http://example.com/index.php/one/two', + 'one/two', + '/index.php/one/two', '', + '', + ['one', 'two'], + 2, + ], + '/one/two' => [ + 'http://example.com/', + 'index.php', + '/one/two', + 'http://example.com/index.php/one/two', + 'one/two', + '/index.php/one/two', + '', + '', + ['one', 'two'], + 2, + ], + 'Subfolder: ' => [ + 'http://example.com/ci4/', + 'index.php', + '', + 'http://example.com/ci4/index.php', + '', + '/ci4/index.php', + '', + '', + [], + 0, + ], + 'Subfolder: one/two' => [ + 'http://example.com/ci4/', + 'index.php', + 'one/two', + 'http://example.com/ci4/index.php/one/two', + 'one/two', + '/ci4/index.php/one/two', + '', + '', + ['one', 'two'], + 2, + ], + 'EmptyIndexPage: ' => [ + 'http://example.com/', + '', + '', + 'http://example.com/', + '', + '/', + '', + '', + [], + 0, + ], + 'EmptyIndexPage: /' => [ + 'http://example.com/', + '', + '/', + 'http://example.com/', + '', + '/', + '', + '', + [], + 0, + ], + ]; + } + + public function provideRelativePathWithQueryOrFragment() + { + return [ + // $baseURL, $indexPage, $relativePath, $expectedURI, $expectedRoutePath, + // $expectedPath, $expectedQuery, $expectedFragment + 'one/two?foo=1&bar=2' => [ + 'http://example.com/', + 'index.php', + 'one/two?foo=1&bar=2', + 'http://example.com/index.php/one/two?foo=1&bar=2', + 'one/two', + '/index.php/one/two', + 'foo=1&bar=2', + '', + ['one', 'two'], + 2, + ], + 'one/two#sec1' => [ + 'http://example.com/', + 'index.php', + 'one/two#sec1', + 'http://example.com/index.php/one/two#sec1', + 'one/two', + '/index.php/one/two', + '', + 'sec1', + ['one', 'two'], + 2, + ], + 'one/two?foo=1&bar=2#sec1' => [ + 'http://example.com/', + 'index.php', + 'one/two?foo=1&bar=2#sec1', + 'http://example.com/index.php/one/two?foo=1&bar=2#sec1', + 'one/two', + '/index.php/one/two', + 'foo=1&bar=2', + 'sec1', + ['one', 'two'], + 2, + ], + 'Subfolder: one/two?foo=1&bar=2' => [ + 'http://example.com/ci4/', + 'index.php', + 'one/two?foo=1&bar=2', + 'http://example.com/ci4/index.php/one/two?foo=1&bar=2', + 'one/two', + '/ci4/index.php/one/two', + 'foo=1&bar=2', + '', + ['one', 'two'], + 2, + ], + ]; } public function testConstructorHost() @@ -137,33 +249,6 @@ public function testConstructorScheme() $this->assertSame('https://example.com/', $uri->getBaseURL()); } - public function testConstructorSubfolder() - { - $config = new App(); - $config->baseURL = 'http://example.com/ci4/'; - - $uri = new SiteURI($config); - - $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('http://example.com/ci4/index.php', (string) $uri); - $this->assertSame('', $uri->getRoutePath()); - $this->assertSame('/ci4/index.php', $uri->getPath()); - $this->assertSame('http://example.com/ci4/', $uri->getBaseURL()); - } - - public function testConstructorSubfolderRelativePathWithQuery() - { - $config = new App(); - $config->baseURL = 'http://example.com/ci4/'; - - $uri = new SiteURI($config, 'one/two?foo=1&bar=2'); - - $this->assertSame('http://example.com/ci4/index.php/one/two?foo=1&bar=2', (string) $uri); - $this->assertSame('one/two', $uri->getRoutePath()); - $this->assertSame('/ci4/index.php/one/two', $uri->getPath()); - $this->assertSame('foo=1&bar=2', $uri->getQuery()); - } - public function testConstructorForceGlobalSecureRequests() { $config = new App(); @@ -175,32 +260,6 @@ public function testConstructorForceGlobalSecureRequests() $this->assertSame('https://example.com/', $uri->getBaseURL()); } - public function testConstructorIndexPageEmpty() - { - $config = new App(); - $config->indexPage = ''; - - $uri = new SiteURI($config); - - $this->assertSame('http://example.com/', (string) $uri); - $this->assertSame('http://example.com/', $uri->getBaseURL()); - $this->assertSame('', $uri->getRoutePath()); - $this->assertSame('/', $uri->getPath()); - } - - public function testConstructorIndexPageEmptyWithPathSlash() - { - $config = new App(); - $config->indexPage = ''; - - $uri = new SiteURI($config, '/'); - - $this->assertSame('http://example.com/', (string) $uri); - $this->assertSame('http://example.com/', $uri->getBaseURL()); - $this->assertSame('', $uri->getRoutePath()); - $this->assertSame('/', $uri->getPath()); - } - public function testConstructorInvalidBaseURL() { $this->expectException(ConfigException::class); @@ -211,83 +270,38 @@ public function testConstructorInvalidBaseURL() new SiteURI($config); } - public function testSetPath() - { - $config = new App(); - - $uri = new SiteURI($config); - - $uri->setPath('test/method'); - - $this->assertSame('http://example.com/index.php/test/method', (string) $uri); - $this->assertSame('test/method', $uri->getRoutePath()); - $this->assertSame('/index.php/test/method', $uri->getPath()); - $this->assertSame(['test', 'method'], $uri->getSegments()); - $this->assertSame('test', $uri->getSegment(1)); - $this->assertSame(2, $uri->getTotalSegments()); - } - - public function testSetPathStartWithSlash() - { - $config = new App(); - - $uri = new SiteURI($config); - - $uri->setPath('/test/method'); - - $this->assertSame('http://example.com/index.php/test/method', (string) $uri); - $this->assertSame('test/method', $uri->getRoutePath()); - $this->assertSame('/index.php/test/method', $uri->getPath()); - $this->assertSame(['test', 'method'], $uri->getSegments()); - $this->assertSame('test', $uri->getSegment(1)); - $this->assertSame(2, $uri->getTotalSegments()); - } - - public function testSetPathSubfolder() - { - $config = new App(); - $config->baseURL = 'http://example.com/ci4/'; - - $uri = new SiteURI($config); - - $uri->setPath('test/method'); - - $this->assertSame('http://example.com/ci4/index.php/test/method', (string) $uri); - $this->assertSame('test/method', $uri->getRoutePath()); - $this->assertSame('/ci4/index.php/test/method', $uri->getPath()); - $this->assertSame(['test', 'method'], $uri->getSegments()); - $this->assertSame('test', $uri->getSegment(1)); - $this->assertSame(2, $uri->getTotalSegments()); - } - - public function testSetPathEmpty() - { - $config = new App(); + /** + * @dataProvider provideURIs + */ + public function testSetPath( + string $baseURL, + string $indexPage, + string $relativePath, + string $expectedURI, + string $expectedRoutePath, + string $expectedPath, + string $expectedQuery, + string $expectedFragment, + array $expectedSegments, + int $expectedTotalSegments + ) { + $config = new App(); + $config->indexPage = $indexPage; + $config->baseURL = $baseURL; $uri = new SiteURI($config); - $uri->setPath(''); + $uri->setPath($relativePath); - $this->assertSame('http://example.com/index.php', (string) $uri); - $this->assertSame('', $uri->getRoutePath()); - $this->assertSame('/index.php', $uri->getPath()); - $this->assertSame([], $uri->getSegments()); - $this->assertSame(0, $uri->getTotalSegments()); - } + $this->assertSame($expectedURI, (string) $uri); + $this->assertSame($expectedRoutePath, $uri->getRoutePath()); + $this->assertSame($expectedPath, $uri->getPath()); + $this->assertSame($expectedQuery, $uri->getQuery()); + $this->assertSame($expectedFragment, $uri->getFragment()); + $this->assertSame($baseURL, $uri->getBaseURL()); - public function testSetPathSlash() - { - $config = new App(); - - $uri = new SiteURI($config); - - $uri->setPath('/'); - - $this->assertSame('http://example.com/index.php/', (string) $uri); - $this->assertSame('', $uri->getRoutePath()); - $this->assertSame('/index.php/', $uri->getPath()); - $this->assertSame([], $uri->getSegments()); - $this->assertSame(0, $uri->getTotalSegments()); + $this->assertSame($expectedSegments, $uri->getSegments()); + $this->assertSame($expectedTotalSegments, $uri->getTotalSegments()); } public function testSetSegment() From ba35e57307be0bff91ac0ffd57416d714deb62cd Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 22 Feb 2023 11:38:34 +0900 Subject: [PATCH 359/485] docs: update comments --- tests/system/HTTP/SiteURITest.php | 44 ++++++++++++++----------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index f7595063bd5e..4473259581a7 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -68,19 +68,17 @@ public function provideConstructor() public function provideURIs() { return [ - // $baseURL, $indexPage, $relativePath, $expectedURI, $expectedRoutePath, - // $expectedPath, $expectedQuery, $expectedFragment '' => [ - 'http://example.com/', - 'index.php', - '', - 'http://example.com/index.php', - '', - '/index.php', - '', - '', - [], - 0, + 'http://example.com/', // $baseURL + 'index.php', // $indexPage + '', // $relativePath + 'http://example.com/index.php', // $expectedURI + '', // $expectedRoutePath + '/index.php', // $expectedPath + '', // $expectedQuery + '', // $expectedFragment + [], // $expectedSegments + 0, // $expectedTotalSegments ], '/' => [ 'http://example.com/', @@ -171,19 +169,17 @@ public function provideURIs() public function provideRelativePathWithQueryOrFragment() { return [ - // $baseURL, $indexPage, $relativePath, $expectedURI, $expectedRoutePath, - // $expectedPath, $expectedQuery, $expectedFragment 'one/two?foo=1&bar=2' => [ - 'http://example.com/', - 'index.php', - 'one/two?foo=1&bar=2', - 'http://example.com/index.php/one/two?foo=1&bar=2', - 'one/two', - '/index.php/one/two', - 'foo=1&bar=2', - '', - ['one', 'two'], - 2, + 'http://example.com/', // $baseURL + 'index.php', // $indexPage + 'one/two?foo=1&bar=2', // $relativePath + 'http://example.com/index.php/one/two?foo=1&bar=2', // $expectedURI + 'one/two', // $expectedRoutePath + '/index.php/one/two', // $expectedPath + 'foo=1&bar=2', // $expectedQuery + '', // $expectedFragment + ['one', 'two'], // $expectedSegments + 2, // $expectedTotalSegments ], 'one/two#sec1' => [ 'http://example.com/', From fe754f3591fc0799395eacbbfcad2849c1ba5c8d Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Feb 2023 10:51:18 +0900 Subject: [PATCH 360/485] fix: change behavior If the path ends with `/`, the URI retain the trailing `/`. --- system/HTTP/SiteURI.php | 122 +++++++++++++++++++----------- tests/system/HTTP/SiteURITest.php | 12 +++ 2 files changed, 91 insertions(+), 43 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index e444dbb7b66a..160a31b4adcb 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -24,7 +24,7 @@ class SiteURI extends URI /** * The current baseURL. */ - private string $baseURL; + private URI $baseURL; /** * The path part of baseURL. @@ -96,32 +96,54 @@ public function __construct( ) { $this->indexPage = $configApp->indexPage; - $baseURL = $this->normalizeBaseURL($configApp); - $this->setBasePath($baseURL); + $this->baseURL = $this->determineBaseURL($configApp, $host, $scheme); - $relativePath = URI::removeDotSegments($relativePath); - // Remove starting slash unless it is `/`. - if ($relativePath !== '' && $relativePath[0] === '/' && $relativePath !== '/') { - $relativePath = ltrim($relativePath, '/'); - } + $this->setBasePath(); - $tempPath = $relativePath; + // Fix routePath, query, fragment + [$routePath, $query, $fragment] = $this->parseRelativePath($relativePath); - // Check for an index page - $indexPage = ''; - if ($configApp->indexPage !== '') { - $indexPage = $configApp->indexPage; + // Fix indexPage and routePath + $indexPageRoutePath = $this->getIndexPageRoutePath($routePath); - // Check if we need a separator - if ($relativePath !== '' && $relativePath[0] !== '/' && $relativePath[0] !== '?') { - $indexPage .= '/'; - } - } elseif ($relativePath === '/') { - $tempPath = ''; + // Fix the current URI + $uri = $this->baseURL . $indexPageRoutePath; + + // applyParts + $parts = parse_url($uri); + if ($parts === false) { + throw HTTPException::forUnableToParseURI($uri); + } + $parts['query'] = $query; + $parts['fragment'] = $fragment; + $this->applyParts($parts); + + $this->setRoutePath($routePath); + } + + private function parseRelativePath(string $relativePath): array + { + $parts = parse_url('http://dummy/' . $relativePath); + if ($parts === false) { + throw HTTPException::forUnableToParseURI($relativePath); } - $tempUri = $baseURL . $indexPage . $tempPath; - $uri = new URI($tempUri); + $routePath = $relativePath === '/' ? '/' : ltrim($parts['path'], '/'); + + $query = $parts['query'] ?? ''; + $fragment = $parts['fragment'] ?? ''; + + return [$routePath, $query, $fragment]; + } + + private function determineBaseURL( + App $configApp, + ?string $host, + ?string $scheme + ): URI { + $baseURL = $this->normalizeBaseURL($configApp); + + $uri = new URI($baseURL); // Update scheme if ($scheme !== null) { @@ -135,24 +157,34 @@ public function __construct( $uri->setHost($host); } - $parts = parse_url((string) $uri); - if ($parts === false) { - throw HTTPException::forUnableToParseURI($uri); + return $uri; + } + + private function getIndexPageRoutePath(string $routePath): string + { + // Remove starting slash unless it is `/`. + if ($routePath !== '' && $routePath[0] === '/' && $routePath !== '/') { + $routePath = ltrim($routePath, '/'); } - $this->applyParts($parts); - // Set routePath - $parts = explode('?', $relativePath); - $parts = explode('#', $parts[0]); - $routePath = $parts[0]; - $this->setRoutePath($routePath); + // Check for an index page + $indexPage = ''; + if ($this->indexPage !== '') { + $indexPage = $this->indexPage; - // Set baseURL - $this->baseURL = URI::createURIString( - $this->getScheme(), - $this->getAuthority(), - $this->basePathWithoutIndexPage, - ); + // Check if we need a separator + if ($routePath !== '' && $routePath[0] !== '/' && $routePath[0] !== '?') { + $indexPage .= '/'; + } + } + + $indexPageRoutePath = $indexPage . $routePath; + + if ($indexPageRoutePath === '/') { + $indexPageRoutePath = ''; + } + + return $indexPageRoutePath; } private function checkHost(string $host, array $allowedHostnames): bool @@ -179,9 +211,9 @@ private function normalizeBaseURL(App $configApp): string /** * Sets basePathWithoutIndexPage and baseSegments. */ - private function setBasePath(string $baseURL): void + private function setBasePath(): void { - $this->basePathWithoutIndexPage = (new URI($baseURL))->getPath(); + $this->basePathWithoutIndexPage = $this->baseURL->getPath(); $this->baseSegments = $this->convertToSegments($this->basePathWithoutIndexPage); @@ -213,7 +245,7 @@ public function setURI(?string $uri = null) */ public function getBaseURL(): string { - return $this->baseURL; + return (string) $this->baseURL; } /** @@ -285,13 +317,17 @@ public function setPath(string $path) /** * Sets the route path (and segments). */ - private function setRoutePath(string $path): void + private function setRoutePath(string $routePath): void { - $this->routePath = $this->filterPath($path); + $routePath = $this->filterPath($routePath); - $this->segments = $this->convertToSegments($this->routePath); + $indexPageRoutePath = $this->getIndexPageRoutePath($routePath); - $this->refreshPath(); + $this->path = $this->basePathWithoutIndexPage . $indexPageRoutePath; + + $this->routePath = ltrim($routePath, '/'); + + $this->segments = $this->convertToSegments($this->routePath); } /** diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 4473259581a7..c9423b6d1da3 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -115,6 +115,18 @@ public function provideURIs() ['one', 'two'], 2, ], + '/one/two/' => [ + 'http://example.com/', + 'index.php', + '/one/two/', + 'http://example.com/index.php/one/two/', + 'one/two/', + '/index.php/one/two/', + '', + '', + ['one', 'two'], + 2, + ], 'Subfolder: ' => [ 'http://example.com/ci4/', 'index.php', From 3f4a9fd94e9a65ca86eaa2e1e441c86fbf4f7cae Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Feb 2023 11:54:35 +0900 Subject: [PATCH 361/485] fix: remove $allowedHostnames check SiteURI will check. --- system/HTTP/SiteURI.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 160a31b4adcb..9d4ccef074d6 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -83,8 +83,7 @@ class SiteURI extends URI /** * @param string $relativePath URI path relative to baseURL. May include * queries or fragments. - * @param string|null $host Optional hostname. If it is not in - * $allowedHostnames, just be ignored. + * @param string|null $host Optional current hostname. * @param string|null $scheme Optional scheme. 'http' or 'https'. * @phpstan-param 'http'|'https'|null $scheme */ @@ -153,7 +152,7 @@ private function determineBaseURL( } // Update host - if ($host !== null && $this->checkHost($host, $configApp->allowedHostnames)) { + if ($host !== null) { $uri->setHost($host); } @@ -187,11 +186,6 @@ private function getIndexPageRoutePath(string $routePath): string return $indexPageRoutePath; } - private function checkHost(string $host, array $allowedHostnames): bool - { - return in_array($host, $allowedHostnames, true); - } - private function normalizeBaseURL(App $configApp): string { // It's possible the user forgot a trailing slash on their From ecf96eb21dc6a97fcadeb09931e9beebf1ae6f5e Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Feb 2023 13:42:59 +0900 Subject: [PATCH 362/485] test: add test cases --- tests/system/HTTP/SiteURITest.php | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index c9423b6d1da3..f2b665b94a38 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -127,6 +127,42 @@ public function provideURIs() ['one', 'two'], 2, ], + '//one/two' => [ + 'http://example.com/', + 'index.php', + '//one/two', + 'http://example.com/index.php/one/two', + 'one/two', + '/index.php/one/two', + '', + '', + ['one', 'two'], + 2, + ], + 'one/two//' => [ + 'http://example.com/', + 'index.php', + 'one/two//', + 'http://example.com/index.php/one/two/', + 'one/two/', + '/index.php/one/two/', + '', + '', + ['one', 'two'], + 2, + ], + '///one///two///' => [ + 'http://example.com/', + 'index.php', + '///one///two///', + 'http://example.com/index.php/one/two/', + 'one/two/', + '/index.php/one/two/', + '', + '', + ['one', 'two'], + 2, + ], 'Subfolder: ' => [ 'http://example.com/ci4/', 'index.php', From 5193414accf0f8411f7d3a5a675c5a22170c76f8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 09:09:57 +0900 Subject: [PATCH 363/485] refactor: remove unneeded override The parent method is fixed. --- system/HTTP/SiteURI.php | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 9d4ccef074d6..fc1071647462 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -252,36 +252,6 @@ public function getRoutePath(): string return $this->routePath; } - /** - * Returns the value of a specific segment of the URI path. - * Allows to get only existing segments or the next one. - * - * @param int $number Segment number starting at 1 - * @param string $default Default value - * - * @return string The value of the segment. If you specify the last +1 - * segment, the $default value. If you specify the last +2 - * or more throws HTTPException. - * - * @TODO remove this method after merging #7267 - */ - public function getSegment(int $number, string $default = ''): string - { - if ($number < 1) { - throw HTTPException::forURISegmentOutOfRange($number); - } - - if ($number > count($this->segments) + 1 && ! $this->silent) { - throw HTTPException::forURISegmentOutOfRange($number); - } - - // The segment should treat the array as 1-based for the user - // but we still have to deal with a zero-based array. - $number--; - - return $this->segments[$number] ?? $default; - } - /** * Formats the URI as a string. */ From b24c01c51287b31f9da18ed2a67628e39e616939 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 10 Jul 2023 14:03:24 +0900 Subject: [PATCH 364/485] docs: fix typo in comments --- system/HTTP/SiteURI.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index fc1071647462..e64080974940 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -43,7 +43,7 @@ class SiteURI extends URI * List of URI segments in baseURL and indexPage. * * If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b", - * and the baseUR is "http://localhost:8888/ci431/public/", then: + * and the baseURL is "http://localhost:8888/ci431/public/", then: * $baseSegments = [ * 0 => 'ci431', * 1 => 'public', @@ -59,7 +59,7 @@ class SiteURI extends URI * to the baseURL. * * If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b", - * and the baseUR is "http://localhost:8888/ci431/public/", then: + * and the baseURL is "http://localhost:8888/ci431/public/", then: * $segments = [ * 0 => 'test', * ]; From 3e349e18774ae6b45f5e9b91e60ba11d5feb72ae Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 10 Jul 2023 14:11:24 +0900 Subject: [PATCH 365/485] docs: make URI::setSilent() deprecated --- system/HTTP/URI.php | 2 +- user_guide_src/source/changelogs/v4.4.0.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 4f12b3464260..0201f0680ce0 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -263,7 +263,7 @@ public function __construct(?string $uri = null) * If $silent == true, then will not throw exceptions and will * attempt to continue gracefully. * - * Note: Method not in PSR-7 + * @deprecated 4.4.0 Method not in PSR-7 * * @return URI */ diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 87712aff5bb6..085489277c7e 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -216,6 +216,7 @@ Deprecations ``$tokenName``, ``$headerName``, ``$expires``, ``$regenerate``, and ``$redirect`` in ``Security`` are deprecated, and no longer used. Use ``$config`` instead. +- **URI:** ``URI::setSilent()`` is deprecated. Bugs Fixed ********** From e529291339008883b6a8dec16a076a35b7d4079a Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 10 Jul 2023 14:30:47 +0900 Subject: [PATCH 366/485] feat: add URI::withScheme() and deprecate URI::setScheme() --- system/HTTP/URI.php | 31 ++++++++++++++++- tests/system/HTTP/URITest.php | 38 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.4.0.rst | 4 ++- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 0201f0680ce0..d27598b6bfb1 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -14,6 +14,7 @@ use BadMethodCallException; use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\App; +use InvalidArgumentException; /** * Abstraction for a uniform resource identifier (URI). @@ -709,7 +710,7 @@ public function setAuthority(string $str) * * @return $this * - * @TODO PSR-7: Should be `withScheme($scheme)`. + * @deprecated Use `withScheme()` instead. */ public function setScheme(string $str) { @@ -719,6 +720,34 @@ public function setScheme(string $str) return $this; } + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * + * @return static A new instance with the specified scheme. + * + * @throws InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme(string $scheme) + { + $uri = clone $this; + + $scheme = strtolower($scheme); + + $uri->scheme = preg_replace('#:(//)?$#', '', $scheme); + + return $uri; + } + /** * Sets the userInfo/Authority portion of the URI. * diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index fa5caafa1a49..ec3443fdf278 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -240,6 +240,44 @@ public function testSetSchemeSetsValue() $this->assertSame($expected, (string) $uri); } + public function testWithScheme() + { + $url = 'example.com'; + $uri = new URI('http://' . $url); + + $new = $uri->withScheme('x'); + + $this->assertSame('x://' . $url, (string) $new); + $this->assertSame('http://' . $url, (string) $uri); + } + + public function testWithSchemeSetsHttps() + { + $url = 'http://example.com/path'; + $uri = new URI($url); + + $new = $uri->withScheme('https'); + + $this->assertSame('https', $new->getScheme()); + $this->assertSame('http', $uri->getScheme()); + + $expected = 'https://example.com/path'; + $this->assertSame($expected, (string) $new); + $expected = 'http://example.com/path'; + $this->assertSame($expected, (string) $uri); + } + + public function testWithSchemeSetsEmpty() + { + $url = 'example.com'; + $uri = new URI('http://' . $url); + + $new = $uri->withScheme(''); + + $this->assertSame($url, (string) $new); + $this->assertSame('http://' . $url, (string) $uri); + } + public function testSetUserInfoSetsValue() { $url = 'http://example.com/path'; diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 085489277c7e..475d08e9eaea 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -216,7 +216,9 @@ Deprecations ``$tokenName``, ``$headerName``, ``$expires``, ``$regenerate``, and ``$redirect`` in ``Security`` are deprecated, and no longer used. Use ``$config`` instead. -- **URI:** ``URI::setSilent()`` is deprecated. +- **URI:** + - ``URI::setSilent()`` is deprecated. + - ``URI::setScheme()`` is deprecated. Use ``withScheme()`` instead. Bugs Fixed ********** From f3bcbfba23438b70b31513cda79de8c4238af644 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jul 2023 12:11:12 +0900 Subject: [PATCH 367/485] refactor: add property types --- app/Config/Routing.php | 2 +- system/Config/Routing.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Config/Routing.php b/app/Config/Routing.php index e3183d2e8db8..8d3c773157cf 100644 --- a/app/Config/Routing.php +++ b/app/Config/Routing.php @@ -78,7 +78,7 @@ class Routing extends BaseRouting * Example: * public $override404 = 'App\Errors::show404'; */ - public $override404; + public ?string $override404 = null; /** * If TRUE, the system will attempt to match the URI against diff --git a/system/Config/Routing.php b/system/Config/Routing.php index 857a27769215..409bcf099f65 100644 --- a/system/Config/Routing.php +++ b/system/Config/Routing.php @@ -76,7 +76,7 @@ class Routing extends BaseConfig * Example: * public $override404 = 'App\Errors::show404'; */ - public $override404; + public ?string $override404 = null; /** * If TRUE, the system will attempt to match the URI against From faac980f67f6df1abcc5ac5d57bb670f32f49d06 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jul 2023 12:11:52 +0900 Subject: [PATCH 368/485] refactor: add return types --- system/Debug/BaseExceptionHandler.php | 2 +- system/HTTP/SiteURI.php | 2 +- system/HotReloader/HotReloader.php | 2 +- system/Router/AutoRouterImproved.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php index 576149b8bf68..9afd450e8b48 100644 --- a/system/Debug/BaseExceptionHandler.php +++ b/system/Debug/BaseExceptionHandler.php @@ -89,7 +89,7 @@ protected function collectVars(Throwable $exception, int $statusCode): array * * @param array|object $trace */ - protected function maskSensitiveData(&$trace, array $keysToMask, string $path = '') + protected function maskSensitiveData(&$trace, array $keysToMask, string $path = ''): void { foreach ($keysToMask as $keyToMask) { $explode = explode('/', $keyToMask); diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index e64080974940..adf873bfb902 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -328,7 +328,7 @@ public function refreshPath() /** * Saves our parts from a parse_url() call. */ - protected function applyParts(array $parts) + protected function applyParts(array $parts): void { if (! empty($parts['host'])) { $this->host = $parts['host']; diff --git a/system/HotReloader/HotReloader.php b/system/HotReloader/HotReloader.php index 2f10fae6a566..564ba544283a 100644 --- a/system/HotReloader/HotReloader.php +++ b/system/HotReloader/HotReloader.php @@ -16,7 +16,7 @@ */ final class HotReloader { - public function run() + public function run(): void { ini_set('zlib.output_compression', 'Off'); diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index a186fd33ef55..6e0583d2a1e9 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -121,7 +121,7 @@ public function __construct(// @phpstan-ignore-line $this->controller = $this->defaultController; } - private function createSegments(string $uri) + private function createSegments(string $uri): array { $segments = explode('/', $uri); $segments = array_filter($segments, static fn ($segment) => $segment !== ''); From 05fc454d6f1a16561b7db34912c1c580dfed4942 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jul 2023 12:13:02 +0900 Subject: [PATCH 369/485] docs: add @return --- system/Exceptions/FrameworkException.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/Exceptions/FrameworkException.php b/system/Exceptions/FrameworkException.php index cee5f903e785..f268966cb6a2 100644 --- a/system/Exceptions/FrameworkException.php +++ b/system/Exceptions/FrameworkException.php @@ -33,6 +33,9 @@ public static function forInvalidFile(string $path) return new static(lang('Core.invalidFile', [$path])); } + /** + * @return static + */ public static function forInvalidDirectory(string $path) { return new static(lang('Core.invalidDirectory', [$path])); From 6318c23a17de375fbd25caeb87f37521a9ae6282 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Feb 2023 13:45:16 +0900 Subject: [PATCH 370/485] feat: add SiteURIFactory --- system/HTTP/IncomingRequest.php | 9 + system/HTTP/SiteURIFactory.php | 251 ++++++++++++++++++ .../SiteURIFactoryDetectRoutePathTest.php | 227 ++++++++++++++++ tests/system/HTTP/SiteURIFactoryTest.php | 91 +++++++ 4 files changed, 578 insertions(+) create mode 100644 system/HTTP/SiteURIFactory.php create mode 100644 tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php create mode 100644 tests/system/HTTP/SiteURIFactoryTest.php diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 2554385b7ba6..99bbf223f405 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -235,6 +235,8 @@ protected function detectURI(string $protocol, string $baseURL) /** * Detects the relative path based on * the URIProtocol Config setting. + * + * @deprecated Moved to SiteURIFactory. */ public function detectPath(string $protocol = ''): string { @@ -265,6 +267,8 @@ public function detectPath(string $protocol = ''): string * fixing the query string if necessary. * * @return string The URI it found. + * + * @deprecated Moved to SiteURIFactory. */ protected function parseRequestURI(): string { @@ -323,6 +327,8 @@ protected function parseRequestURI(): string * Parse QUERY_STRING * * Will parse QUERY_STRING and automatically detect the URI from it. + * + * @deprecated Moved to SiteURIFactory. */ protected function parseQueryString(): string { @@ -495,6 +501,9 @@ public function setPath(string $path, ?App $config = null) return $this; } + /** + * @deprecated Moved to SiteURIFactory. + */ private function determineHost(App $config, string $baseURL): string { $host = parse_url($baseURL, PHP_URL_HOST); diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php new file mode 100644 index 000000000000..e96cc2781f85 --- /dev/null +++ b/system/HTTP/SiteURIFactory.php @@ -0,0 +1,251 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\HTTP\Exceptions\HTTPException; +use Config\App; + +class SiteURIFactory +{ + /** + * @var array Superglobal SERVER array + */ + private array $server; + + private App $appConfig; + + /** + * @param array $server Superglobal $_SERVER array + */ + public function __construct(array $server, App $appConfig) + { + $this->server = $server; + $this->appConfig = $appConfig; + } + + /** + * Create the current URI object from superglobals. + * + * This method updates superglobal $_SERVER and $_GET. + */ + public function createFromGlobals(): SiteURI + { + $routePath = $this->detectRoutePath(); + + return $this->createURIFromRoutePath($routePath); + } + + /** + * Create the SiteURI object from URI string. + * + * @internal Used for testing purposes only. + */ + public function createFromString(string $uri): SiteURI + { + // Validate URI + if (filter_var($uri, FILTER_VALIDATE_URL) === false) { + throw HTTPException::forUnableToParseURI($uri); + } + + $parts = parse_url($uri); + + if ($parts === false) { + throw HTTPException::forUnableToParseURI($uri); + } + + $query = $fragment = ''; + if (isset($parts['query'])) { + $query = '?' . $parts['query']; + } + if (isset($parts['fragment'])) { + $fragment = '#' . $parts['fragment']; + } + + $relativePath = $parts['path'] . $query . $fragment; + + return new SiteURI($this->appConfig, $relativePath, $parts['host'], $parts['scheme']); + } + + /** + * Detects the current URI path relative to baseURL based on the URIProtocol + * Config setting. + * + * @param string $protocol URIProtocol + * + * @return string The route path + * + * @internal Used for testing purposes only. + */ + public function detectRoutePath(string $protocol = ''): string + { + if ($protocol === '') { + $protocol = $this->appConfig->uriProtocol; + } + + switch ($protocol) { + case 'REQUEST_URI': + $routePath = $this->parseRequestURI(); + break; + + case 'QUERY_STRING': + $routePath = $this->parseQueryString(); + break; + + case 'PATH_INFO': + default: + $routePath = $this->server[$protocol] ?? $this->parseRequestURI(); + break; + } + + return ($routePath === '/' || $routePath === '') ? '/' : ltrim($routePath, '/'); + } + + /** + * Will parse the REQUEST_URI and automatically detect the URI from it, + * fixing the query string if necessary. + * + * This method updates superglobal $_SERVER and $_GET. + * + * @return string The route path (before normalization). + */ + private function parseRequestURI(): string + { + if (! isset($this->server['REQUEST_URI'], $this->server['SCRIPT_NAME'])) { + return ''; + } + + // parse_url() returns false if no host is present, but the path or query + // string contains a colon followed by a number. So we attach a dummy + // host since REQUEST_URI does not include the host. This allows us to + // parse out the query string and path. + $parts = parse_url('http://dummy' . $this->server['REQUEST_URI']); + $query = $parts['query'] ?? ''; + $path = $parts['path'] ?? ''; + + // Strip the SCRIPT_NAME path from the URI + if ( + $path !== '' && isset($this->server['SCRIPT_NAME'][0]) + && pathinfo($this->server['SCRIPT_NAME'], PATHINFO_EXTENSION) === 'php' + ) { + // Compare each segment, dropping them until there is no match + $segments = $keep = explode('/', $path); + + foreach (explode('/', $this->server['SCRIPT_NAME']) as $i => $segment) { + // If these segments are not the same then we're done + if (! isset($segments[$i]) || $segment !== $segments[$i]) { + break; + } + + array_shift($keep); + } + + $path = implode('/', $keep); + } + + // This section ensures that even on servers that require the URI to + // contain the query string (Nginx) a correct URI is found, and also + // fixes the QUERY_STRING Server var and $_GET array. + if (trim($path, '/') === '' && strncmp($query, '/', 1) === 0) { + $parts = explode('?', $query, 2); + $path = $parts[0]; + $newQuery = $query[1] ?? ''; + + $this->server['QUERY_STRING'] = $newQuery; + $this->updateServer('QUERY_STRING', $newQuery); + } else { + $this->server['QUERY_STRING'] = $query; + $this->updateServer('QUERY_STRING', $query); + } + + // Update our global GET for values likely to have been changed + parse_str($this->server['QUERY_STRING'], $get); + $this->updateGetArray($get); + + return URI::removeDotSegments($path); + } + + private function updateServer(string $key, string $value): void + { + $_SERVER[$key] = $value; + } + + private function updateGetArray(array $array): void + { + $_GET = $array; + } + + /** + * Will parse QUERY_STRING and automatically detect the URI from it. + * + * This method updates superglobal $_SERVER and $_GET. + * + * @return string The route path (before normalization). + */ + private function parseQueryString(): string + { + $query = $this->server['QUERY_STRING'] ?? @getenv('QUERY_STRING'); + + if (trim($query, '/') === '') { + return '/'; + } + + if (strncmp($query, '/', 1) === 0) { + $parts = explode('?', $query, 2); + $path = $parts[0]; + $newQuery = $parts[1] ?? ''; + + $this->server['QUERY_STRING'] = $newQuery; + $this->updateServer('QUERY_STRING', $newQuery); + } else { + $path = $query; + } + + // Update our global GET for values likely to have been changed + parse_str($this->server['QUERY_STRING'], $get); + $this->updateGetArray($get); + + return URI::removeDotSegments($path); + } + + /** + * Create current URI object. + * + * @param string $routePath URI path relative to baseURL + */ + private function createURIFromRoutePath(string $routePath): SiteURI + { + $query = $this->server['QUERY_STRING'] ?? ''; + + $relativePath = $query !== '' ? $routePath . '?' . $query : $routePath; + + return new SiteURI($this->appConfig, $relativePath, $this->getHost()); + } + + /** + * @return string|null The current hostname. Returns null if no host header. + */ + private function getHost(): ?string + { + $host = null; + + $httpHostPort = $this->server['HTTP_HOST'] ?? null; + if ($httpHostPort !== null) { + [$httpHost] = explode(':', $httpHostPort, 2); + + if (in_array($httpHost, $this->appConfig->allowedHostnames, true)) { + $host = $httpHost; + } + } + + return $host; + } +} diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php new file mode 100644 index 000000000000..3b7715049a5c --- /dev/null +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -0,0 +1,227 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\Test\CIUnitTestCase; +use Config\App; + +/** + * @backupGlobals enabled + * + * @internal + * + * @group Others + */ +final class SiteURIFactoryDetectRoutePathTest extends CIUnitTestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $_GET = $_SERVER = []; + } + + private function createSiteURIFactory(array $server, ?App $appConfig = null): SiteURIFactory + { + $appConfig ??= new App(); + + return new SiteURIFactory($server, $appConfig); + } + + public function testDefault() + { + // /index.php/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath()); + } + + public function testDefaultEmpty() + { + // / + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = '/'; + $this->assertSame($expected, $factory->detectRoutePath()); + } + + public function testRequestURI() + { + // /index.php/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURINested() + { + // I'm not sure but this is a case of Apache config making such SERVER + // values? + // The current implementation doesn't use the value of the URI object. + // So I removed the code to set URI. Therefore, it's exactly the same as + // the method above as a test. + // But it may be changed in the future to use the value of the URI object. + // So I don't remove this test case. + + // /ci/index.php/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURISubfolder() + { + // /ci/index.php/popcorn/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/ci/index.php/popcorn/woot'; + $_SERVER['SCRIPT_NAME'] = '/ci/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = 'popcorn/woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURINoIndex() + { + // /sub/example + $_SERVER['REQUEST_URI'] = '/sub/example'; + $_SERVER['SCRIPT_NAME'] = '/sub/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = 'example'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURINginx() + { + // /ci/index.php/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php/woot?code=good'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURINginxRedirecting() + { + // /?/ci/index.php/woot + $_SERVER['REQUEST_URI'] = '/?/ci/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = 'ci/woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURISuppressed() + { + // /woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/woot'; + $_SERVER['SCRIPT_NAME'] = '/'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testQueryString() + { + // /index.php?/ci/woot + $_SERVER['REQUEST_URI'] = '/index.php?/ci/woot'; + $_SERVER['QUERY_STRING'] = '/ci/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $_GET['/ci/woot'] = ''; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = 'ci/woot'; + $this->assertSame($expected, $factory->detectRoutePath('QUERY_STRING')); + } + + public function testQueryStringWithQueryString() + { + // /index.php?/ci/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php?/ci/woot?code=good'; + $_SERVER['QUERY_STRING'] = '/ci/woot?code=good'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $_GET['/ci/woot?code'] = 'good'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = 'ci/woot'; + $this->assertSame($expected, $factory->detectRoutePath('QUERY_STRING')); + $this->assertSame('code=good', $_SERVER['QUERY_STRING']); + $this->assertSame(['code' => 'good'], $_GET); + } + + public function testQueryStringEmpty() + { + // /index.php? + $_SERVER['REQUEST_URI'] = '/index.php?'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = '/'; + $this->assertSame($expected, $factory->detectRoutePath('QUERY_STRING')); + } + + public function testPathInfoUnset() + { + // /index.php/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('PATH_INFO')); + } + + public function testPathInfoSubfolder() + { + $appConfig = new App(); + $appConfig->baseURL = 'http://localhost:8888/ci431/public/'; + + // http://localhost:8888/ci431/public/index.php/woot?code=good#pos + $_SERVER['PATH_INFO'] = '/woot'; + $_SERVER['REQUEST_URI'] = '/ci431/public/index.php/woot?code=good'; + $_SERVER['SCRIPT_NAME'] = '/ci431/public/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER, $appConfig); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('PATH_INFO')); + } +} diff --git a/tests/system/HTTP/SiteURIFactoryTest.php b/tests/system/HTTP/SiteURIFactoryTest.php new file mode 100644 index 000000000000..294ae9e9844b --- /dev/null +++ b/tests/system/HTTP/SiteURIFactoryTest.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\Test\CIUnitTestCase; +use Config\App; + +/** + * @backupGlobals enabled + * + * @internal + * + * @group Others + */ +final class SiteURIFactoryTest extends CIUnitTestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $_GET = $_SERVER = []; + } + + public function testCreateFromGlobals() + { + // http://localhost:8080/index.php/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php/woot?code=good'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['QUERY_STRING'] = 'code=good'; + $_SERVER['HTTP_HOST'] = 'localhost:8080'; + $_SERVER['PATH_INFO'] = '/woot'; + + $_GET['code'] = 'good'; + + $factory = new SiteURIFactory($_SERVER, new App()); + + $uri = $factory->createFromGlobals(); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://localhost:8080/index.php/woot?code=good', (string) $uri); + $this->assertSame('/index.php/woot', $uri->getPath()); + $this->assertSame('woot', $uri->getRoutePath()); + } + + public function testCreateFromGlobalsAllowedHost() + { + // http://users.example.jp/index.php/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php/woot?code=good'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['QUERY_STRING'] = 'code=good'; + $_SERVER['HTTP_HOST'] = 'users.example.jp'; + $_SERVER['PATH_INFO'] = '/woot'; + + $_GET['code'] = 'good'; + + $config = new App(); + $config->baseURL = 'http://example.jp/'; + $config->allowedHostnames = ['users.example.jp']; + + $factory = new SiteURIFactory($_SERVER, $config); + + $uri = $factory->createFromGlobals(); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://users.example.jp/index.php/woot?code=good', (string) $uri); + $this->assertSame('/index.php/woot', $uri->getPath()); + $this->assertSame('woot', $uri->getRoutePath()); + } + + public function testCreateFromString() + { + $factory = new SiteURIFactory($_SERVER, new App()); + + $uriString = 'http://invalid.example.jp/foo/bar?page=3'; + $uri = $factory->createFromString($uriString); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://localhost:8080/index.php/foo/bar?page=3', (string) $uri); + $this->assertSame('/index.php/foo/bar', $uri->getPath()); + $this->assertSame('foo/bar', $uri->getRoutePath()); + } +} From 09a96abb01315a8f8a0d1c2097f2870c6a893eef Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Feb 2023 16:06:03 +0900 Subject: [PATCH 371/485] fix: createFromString() returns URI with invalid hostname --- system/HTTP/SiteURIFactory.php | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index e96cc2781f85..0115ef5df9d5 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -71,8 +71,9 @@ public function createFromString(string $uri): SiteURI } $relativePath = $parts['path'] . $query . $fragment; + $host = $this->getValidHost($parts['host']); - return new SiteURI($this->appConfig, $relativePath, $parts['host'], $parts['scheme']); + return new SiteURI($this->appConfig, $relativePath, $host, $parts['scheme']); } /** @@ -231,21 +232,30 @@ private function createURIFromRoutePath(string $routePath): SiteURI } /** - * @return string|null The current hostname. Returns null if no host header. + * @return string|null The current hostname. Returns null if no valid host. */ private function getHost(): ?string { - $host = null; - $httpHostPort = $this->server['HTTP_HOST'] ?? null; + if ($httpHostPort !== null) { [$httpHost] = explode(':', $httpHostPort, 2); - if (in_array($httpHost, $this->appConfig->allowedHostnames, true)) { - $host = $httpHost; - } + return $this->getValidHost($httpHost); + } + + return null; + } + + /** + * @return string|null The valid hostname. Returns null if not valid. + */ + private function getValidHost(string $host): ?string + { + if (in_array($host, $this->appConfig->allowedHostnames, true)) { + return $host; } - return $host; + return null; } } From 37772e910e7ddaab91185b8a8813d2977fd2678c Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Jul 2023 08:20:45 +0900 Subject: [PATCH 372/485] docs: add version to @deprecated --- system/HTTP/IncomingRequest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 99bbf223f405..bf9e01a8a603 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -236,7 +236,7 @@ protected function detectURI(string $protocol, string $baseURL) * Detects the relative path based on * the URIProtocol Config setting. * - * @deprecated Moved to SiteURIFactory. + * @deprecated 4.4.0 Moved to SiteURIFactory. */ public function detectPath(string $protocol = ''): string { @@ -268,7 +268,7 @@ public function detectPath(string $protocol = ''): string * * @return string The URI it found. * - * @deprecated Moved to SiteURIFactory. + * @deprecated 4.4.0 Moved to SiteURIFactory. */ protected function parseRequestURI(): string { @@ -328,7 +328,7 @@ protected function parseRequestURI(): string * * Will parse QUERY_STRING and automatically detect the URI from it. * - * @deprecated Moved to SiteURIFactory. + * @deprecated 4.4.0 Moved to SiteURIFactory. */ protected function parseQueryString(): string { @@ -502,7 +502,7 @@ public function setPath(string $path, ?App $config = null) } /** - * @deprecated Moved to SiteURIFactory. + * @deprecated 4.4.0 Moved to SiteURIFactory. */ private function determineHost(App $config, string $baseURL): string { From 9937a0308a4a288a43f1c53d36a18407a5ecf9f3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Jul 2023 09:32:19 +0900 Subject: [PATCH 373/485] refactor: extract class Superglobals and use it --- system/HTTP/SiteURIFactory.php | 64 +++++++------------ system/Superglobals.php | 35 ++++++++++ .../SiteURIFactoryDetectRoutePathTest.php | 6 +- tests/system/HTTP/SiteURIFactoryTest.php | 10 ++- 4 files changed, 71 insertions(+), 44 deletions(-) create mode 100644 system/Superglobals.php diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index 0115ef5df9d5..dbe29546963e 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -12,24 +12,18 @@ namespace CodeIgniter\HTTP; use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\Superglobals; use Config\App; class SiteURIFactory { - /** - * @var array Superglobal SERVER array - */ - private array $server; - + private Superglobals $superglobals; private App $appConfig; - /** - * @param array $server Superglobal $_SERVER array - */ - public function __construct(array $server, App $appConfig) + public function __construct(Superglobals $superglobals, App $appConfig) { - $this->server = $server; - $this->appConfig = $appConfig; + $this->superglobals = $superglobals; + $this->appConfig = $appConfig; } /** @@ -103,7 +97,7 @@ public function detectRoutePath(string $protocol = ''): string case 'PATH_INFO': default: - $routePath = $this->server[$protocol] ?? $this->parseRequestURI(); + $routePath = $this->superglobals->server($protocol) ?? $this->parseRequestURI(); break; } @@ -120,7 +114,10 @@ public function detectRoutePath(string $protocol = ''): string */ private function parseRequestURI(): string { - if (! isset($this->server['REQUEST_URI'], $this->server['SCRIPT_NAME'])) { + if ( + $this->superglobals->server('REQUEST_URI') === null + || $this->superglobals->server('SCRIPT_NAME') === null + ) { return ''; } @@ -128,19 +125,19 @@ private function parseRequestURI(): string // string contains a colon followed by a number. So we attach a dummy // host since REQUEST_URI does not include the host. This allows us to // parse out the query string and path. - $parts = parse_url('http://dummy' . $this->server['REQUEST_URI']); + $parts = parse_url('http://dummy' . $this->superglobals->server('REQUEST_URI')); $query = $parts['query'] ?? ''; $path = $parts['path'] ?? ''; // Strip the SCRIPT_NAME path from the URI if ( - $path !== '' && isset($this->server['SCRIPT_NAME'][0]) - && pathinfo($this->server['SCRIPT_NAME'], PATHINFO_EXTENSION) === 'php' + $path !== '' && isset($this->superglobals->server('SCRIPT_NAME')[0]) + && pathinfo($this->superglobals->server('SCRIPT_NAME'), PATHINFO_EXTENSION) === 'php' ) { // Compare each segment, dropping them until there is no match $segments = $keep = explode('/', $path); - foreach (explode('/', $this->server['SCRIPT_NAME']) as $i => $segment) { + foreach (explode('/', $this->superglobals->server('SCRIPT_NAME')) as $i => $segment) { // If these segments are not the same then we're done if (! isset($segments[$i]) || $segment !== $segments[$i]) { break; @@ -160,30 +157,18 @@ private function parseRequestURI(): string $path = $parts[0]; $newQuery = $query[1] ?? ''; - $this->server['QUERY_STRING'] = $newQuery; - $this->updateServer('QUERY_STRING', $newQuery); + $this->superglobals->setServer('QUERY_STRING', $newQuery); } else { - $this->server['QUERY_STRING'] = $query; - $this->updateServer('QUERY_STRING', $query); + $this->superglobals->setServer('QUERY_STRING', $query); } // Update our global GET for values likely to have been changed - parse_str($this->server['QUERY_STRING'], $get); - $this->updateGetArray($get); + parse_str($this->superglobals->server('QUERY_STRING'), $get); + $this->superglobals->setGetArray($get); return URI::removeDotSegments($path); } - private function updateServer(string $key, string $value): void - { - $_SERVER[$key] = $value; - } - - private function updateGetArray(array $array): void - { - $_GET = $array; - } - /** * Will parse QUERY_STRING and automatically detect the URI from it. * @@ -193,7 +178,7 @@ private function updateGetArray(array $array): void */ private function parseQueryString(): string { - $query = $this->server['QUERY_STRING'] ?? @getenv('QUERY_STRING'); + $query = $this->superglobals->server('QUERY_STRING') ?? @getenv('QUERY_STRING'); if (trim($query, '/') === '') { return '/'; @@ -204,15 +189,14 @@ private function parseQueryString(): string $path = $parts[0]; $newQuery = $parts[1] ?? ''; - $this->server['QUERY_STRING'] = $newQuery; - $this->updateServer('QUERY_STRING', $newQuery); + $this->superglobals->setServer('QUERY_STRING', $newQuery); } else { $path = $query; } // Update our global GET for values likely to have been changed - parse_str($this->server['QUERY_STRING'], $get); - $this->updateGetArray($get); + parse_str($this->superglobals->server('QUERY_STRING'), $get); + $this->superglobals->setGetArray($get); return URI::removeDotSegments($path); } @@ -224,7 +208,7 @@ private function parseQueryString(): string */ private function createURIFromRoutePath(string $routePath): SiteURI { - $query = $this->server['QUERY_STRING'] ?? ''; + $query = $this->superglobals->server('QUERY_STRING') ?? ''; $relativePath = $query !== '' ? $routePath . '?' . $query : $routePath; @@ -236,7 +220,7 @@ private function createURIFromRoutePath(string $routePath): SiteURI */ private function getHost(): ?string { - $httpHostPort = $this->server['HTTP_HOST'] ?? null; + $httpHostPort = $this->superglobals->server('HTTP_HOST') ?? null; if ($httpHostPort !== null) { [$httpHost] = explode(':', $httpHostPort, 2); diff --git a/system/Superglobals.php b/system/Superglobals.php new file mode 100644 index 000000000000..65ab800e98f3 --- /dev/null +++ b/system/Superglobals.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter; + +/** + * Superglobals manipulation. + * + * @internal + */ +final class Superglobals +{ + public function server(string $key): ?string + { + return $_SERVER[$key] ?? null; + } + + public function setServer(string $key, string $value): void + { + $_SERVER[$key] = $value; + } + + public function setGetArray(array $array): void + { + $_GET = $array; + } +} diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index 3b7715049a5c..12d111f33390 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Superglobals; use CodeIgniter\Test\CIUnitTestCase; use Config\App; @@ -34,7 +35,10 @@ private function createSiteURIFactory(array $server, ?App $appConfig = null): Si { $appConfig ??= new App(); - return new SiteURIFactory($server, $appConfig); + $_SERVER = $server; + $superglobals = new Superglobals(); + + return new SiteURIFactory($superglobals, $appConfig); } public function testDefault() diff --git a/tests/system/HTTP/SiteURIFactoryTest.php b/tests/system/HTTP/SiteURIFactoryTest.php index 294ae9e9844b..12ee43757579 100644 --- a/tests/system/HTTP/SiteURIFactoryTest.php +++ b/tests/system/HTTP/SiteURIFactoryTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Superglobals; use CodeIgniter\Test\CIUnitTestCase; use Config\App; @@ -41,7 +42,8 @@ public function testCreateFromGlobals() $_GET['code'] = 'good'; - $factory = new SiteURIFactory($_SERVER, new App()); + $superglobals = new Superglobals(); + $factory = new SiteURIFactory($superglobals, new App()); $uri = $factory->createFromGlobals(); @@ -66,7 +68,8 @@ public function testCreateFromGlobalsAllowedHost() $config->baseURL = 'http://example.jp/'; $config->allowedHostnames = ['users.example.jp']; - $factory = new SiteURIFactory($_SERVER, $config); + $superglobals = new Superglobals(); + $factory = new SiteURIFactory($superglobals, $config); $uri = $factory->createFromGlobals(); @@ -78,7 +81,8 @@ public function testCreateFromGlobalsAllowedHost() public function testCreateFromString() { - $factory = new SiteURIFactory($_SERVER, new App()); + $superglobals = new Superglobals(); + $factory = new SiteURIFactory($superglobals, new App()); $uriString = 'http://invalid.example.jp/foo/bar?page=3'; $uri = $factory->createFromString($uriString); From 8522e992208ca795f16254156cbed21a55ea82b1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Jul 2023 09:43:19 +0900 Subject: [PATCH 374/485] test: extract createSiteURIFactory() --- tests/system/HTTP/SiteURIFactoryTest.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/system/HTTP/SiteURIFactoryTest.php b/tests/system/HTTP/SiteURIFactoryTest.php index 12ee43757579..e211e7167db5 100644 --- a/tests/system/HTTP/SiteURIFactoryTest.php +++ b/tests/system/HTTP/SiteURIFactoryTest.php @@ -31,6 +31,14 @@ protected function setUp(): void $_GET = $_SERVER = []; } + private function createSiteURIFactory(?App $config = null, ?Superglobals $superglobals = null): SiteURIFactory + { + $config ??= new App(); + $superglobals ??= new Superglobals(); + + return new SiteURIFactory($superglobals, $config); + } + public function testCreateFromGlobals() { // http://localhost:8080/index.php/woot?code=good#pos @@ -42,8 +50,7 @@ public function testCreateFromGlobals() $_GET['code'] = 'good'; - $superglobals = new Superglobals(); - $factory = new SiteURIFactory($superglobals, new App()); + $factory = $this->createSiteURIFactory(); $uri = $factory->createFromGlobals(); @@ -68,8 +75,7 @@ public function testCreateFromGlobalsAllowedHost() $config->baseURL = 'http://example.jp/'; $config->allowedHostnames = ['users.example.jp']; - $superglobals = new Superglobals(); - $factory = new SiteURIFactory($superglobals, $config); + $factory = $this->createSiteURIFactory($config); $uri = $factory->createFromGlobals(); @@ -81,8 +87,7 @@ public function testCreateFromGlobalsAllowedHost() public function testCreateFromString() { - $superglobals = new Superglobals(); - $factory = new SiteURIFactory($superglobals, new App()); + $factory = $this->createSiteURIFactory(); $uriString = 'http://invalid.example.jp/foo/bar?page=3'; $uri = $factory->createFromString($uriString); From be7ca15611603c7ff7f9558e0f5fae03061c8ab5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Jul 2023 09:44:44 +0900 Subject: [PATCH 375/485] refactor: change parameter order --- system/HTTP/SiteURIFactory.php | 6 +++--- tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php | 2 +- tests/system/HTTP/SiteURIFactoryTest.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index dbe29546963e..c8996038116f 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -17,13 +17,13 @@ class SiteURIFactory { - private Superglobals $superglobals; private App $appConfig; + private Superglobals $superglobals; - public function __construct(Superglobals $superglobals, App $appConfig) + public function __construct(App $appConfig, Superglobals $superglobals) { - $this->superglobals = $superglobals; $this->appConfig = $appConfig; + $this->superglobals = $superglobals; } /** diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index 12d111f33390..b207d1134dc4 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -38,7 +38,7 @@ private function createSiteURIFactory(array $server, ?App $appConfig = null): Si $_SERVER = $server; $superglobals = new Superglobals(); - return new SiteURIFactory($superglobals, $appConfig); + return new SiteURIFactory($appConfig, $superglobals); } public function testDefault() diff --git a/tests/system/HTTP/SiteURIFactoryTest.php b/tests/system/HTTP/SiteURIFactoryTest.php index e211e7167db5..138ccad0b48d 100644 --- a/tests/system/HTTP/SiteURIFactoryTest.php +++ b/tests/system/HTTP/SiteURIFactoryTest.php @@ -36,7 +36,7 @@ private function createSiteURIFactory(?App $config = null, ?Superglobals $superg $config ??= new App(); $superglobals ??= new Superglobals(); - return new SiteURIFactory($superglobals, $config); + return new SiteURIFactory($config, $superglobals); } public function testCreateFromGlobals() From 1a95c89421e9ea25282e201c7f1c3c39f1eb84f2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Jul 2023 09:50:40 +0900 Subject: [PATCH 376/485] feat: allow Superglobals state changes from the constructor --- system/Superglobals.php | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/system/Superglobals.php b/system/Superglobals.php index 65ab800e98f3..4ffa51033bac 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -18,18 +18,37 @@ */ final class Superglobals { + private array $server; + private array $get; + + public function __construct(?array $server = null, ?array $get = null) + { + $this->server = $server ?? $_SERVER; + $this->get = $get ?? $_GET; + } + public function server(string $key): ?string { - return $_SERVER[$key] ?? null; + return $this->server[$key] ?? null; } public function setServer(string $key, string $value): void { - $_SERVER[$key] = $value; + $this->server[$key] = $value; + $_SERVER[$key] = $value; + } + + /** + * @return array|string|null + */ + public function get(string $key) + { + return $this->get[$key] ?? null; } public function setGetArray(array $array): void { - $_GET = $array; + $this->get = $array; + $_GET = $array; } } From 78b0ac6ef9d1fed2a48d16d74b1389a550146d7e Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 09:32:33 +0900 Subject: [PATCH 377/485] refactore: remove `@` that does not make sense, and cast to string Because it might be false. --- system/HTTP/SiteURIFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index c8996038116f..49ee3bf3ed73 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -178,7 +178,7 @@ private function parseRequestURI(): string */ private function parseQueryString(): string { - $query = $this->superglobals->server('QUERY_STRING') ?? @getenv('QUERY_STRING'); + $query = $this->superglobals->server('QUERY_STRING') ?? (string) getenv('QUERY_STRING'); if (trim($query, '/') === '') { return '/'; From fde3216efdb84392aeb7c53df1f2df2c0a6bd74f Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 09:37:34 +0900 Subject: [PATCH 378/485] refactor: remove string array access It is difficult to understand. --- system/HTTP/SiteURIFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index 49ee3bf3ed73..fcf295c82b02 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -131,7 +131,7 @@ private function parseRequestURI(): string // Strip the SCRIPT_NAME path from the URI if ( - $path !== '' && isset($this->superglobals->server('SCRIPT_NAME')[0]) + $path !== '' && $this->superglobals->server('SCRIPT_NAME') !== '' && pathinfo($this->superglobals->server('SCRIPT_NAME'), PATHINFO_EXTENSION) === 'php' ) { // Compare each segment, dropping them until there is no match From 6b3dd7af0da55785d6342883634738db8e0779fe Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 09:40:55 +0900 Subject: [PATCH 379/485] refactor: make SiteURIFactory final --- system/HTTP/SiteURIFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index fcf295c82b02..f0b846a44457 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -15,7 +15,7 @@ use CodeIgniter\Superglobals; use Config\App; -class SiteURIFactory +final class SiteURIFactory { private App $appConfig; private Superglobals $superglobals; From 84b4a31535cd767225a0cdcdeb40c7170fefc1c5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 09:50:00 +0900 Subject: [PATCH 380/485] feat: add Superglobals::setGet() --- system/Superglobals.php | 6 ++++++ tests/system/SuperglobalsTest.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 tests/system/SuperglobalsTest.php diff --git a/system/Superglobals.php b/system/Superglobals.php index 4ffa51033bac..b126af130f34 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -46,6 +46,12 @@ public function get(string $key) return $this->get[$key] ?? null; } + public function setGet(string $key, string $value): void + { + $this->get[$key] = $value; + $_GET[$key] = $value; + } + public function setGetArray(array $array): void { $this->get = $array; diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php new file mode 100644 index 000000000000..f530145da766 --- /dev/null +++ b/tests/system/SuperglobalsTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter; + +use CodeIgniter\Test\CIUnitTestCase; + +/** + * @internal + * + * @group Others + */ +final class SuperglobalsTest extends CIUnitTestCase +{ + public function testSetGet() + { + $globals = new Superglobals([], []); + + $globals->setGet('test', 'value1'); + + $this->assertSame('value1', $globals->get('test')); + } +} From 23539c95ff1d3a70ff5ce9f396678c40460f90d6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Jul 2023 06:26:47 +0900 Subject: [PATCH 381/485] fix: merge Exception::maskSensitiveData() fix into BaseExceptionHandler --- system/Debug/BaseExceptionHandler.php | 45 +++++++---- system/Debug/Exceptions.php | 4 +- tests/system/Debug/ExceptionHandlerTest.php | 82 +++++++++++++++++++++ 3 files changed, 117 insertions(+), 14 deletions(-) diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php index 9afd450e8b48..0a4bfc8eeb1c 100644 --- a/system/Debug/BaseExceptionHandler.php +++ b/system/Debug/BaseExceptionHandler.php @@ -70,7 +70,7 @@ protected function collectVars(Throwable $exception, int $statusCode): array $trace = $exception->getTrace(); if ($this->config->sensitiveDataInTrace !== []) { - $this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace); + $trace = $this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace); } return [ @@ -89,30 +89,49 @@ protected function collectVars(Throwable $exception, int $statusCode): array * * @param array|object $trace */ - protected function maskSensitiveData(&$trace, array $keysToMask, string $path = ''): void + protected function maskSensitiveData($trace, array $keysToMask, string $path = '') + { + foreach ($trace as $i => $line) { + $trace[$i]['args'] = $this->maskData($line['args'], $keysToMask); + } + + return $trace; + } + + /** + * @param array|object $args + * + * @return array|object + */ + private function maskData($args, array $keysToMask, string $path = '') { foreach ($keysToMask as $keyToMask) { $explode = explode('/', $keyToMask); $index = end($explode); if (strpos(strrev($path . '/' . $index), strrev($keyToMask)) === 0) { - if (is_array($trace) && array_key_exists($index, $trace)) { - $trace[$index] = '******************'; - } elseif (is_object($trace) && property_exists($trace, $index) && isset($trace->{$index})) { - $trace->{$index} = '******************'; + if (is_array($args) && array_key_exists($index, $args)) { + $args[$index] = '******************'; + } elseif ( + is_object($args) && property_exists($args, $index) + && isset($args->{$index}) && is_scalar($args->{$index}) + ) { + $args->{$index} = '******************'; } } } - if (is_object($trace)) { - $trace = get_object_vars($trace); - } - - if (is_array($trace)) { - foreach ($trace as $pathKey => $subarray) { - $this->maskSensitiveData($subarray, $keysToMask, $path . '/' . $pathKey); + if (is_array($args)) { + foreach ($args as $pathKey => $subarray) { + $args[$pathKey] = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey); + } + } elseif (is_object($args)) { + foreach ($args as $pathKey => $subarray) { + $args->{$pathKey} = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey); } } + + return $args; } /** diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 3a7c0e4a3c39..d185070412a2 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -338,7 +338,7 @@ protected function collectVars(Throwable $exception, int $statusCode): array * * @return array|object * - * @deprecated No longer used. Moved to BaseExceptionHandler. + * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ protected function maskSensitiveData($trace, array $keysToMask, string $path = '') { @@ -353,6 +353,8 @@ protected function maskSensitiveData($trace, array $keysToMask, string $path = ' * @param array|object $args * * @return array|object + * + * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ private function maskData($args, array $keysToMask, string $path = '') { diff --git a/tests/system/Debug/ExceptionHandlerTest.php b/tests/system/Debug/ExceptionHandlerTest.php index f28f44a9b7e4..cd87eff739cf 100644 --- a/tests/system/Debug/ExceptionHandlerTest.php +++ b/tests/system/Debug/ExceptionHandlerTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Debug; +use App\Controllers\Home; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; @@ -140,4 +141,85 @@ public function testHandleCLIPageNotFoundException(): void $this->resetStreamFilterBuffer(); } + + public function testMaskSensitiveData(): void + { + $maskSensitiveData = $this->getPrivateMethodInvoker($this->handler, 'maskSensitiveData'); + + $trace = [ + 0 => [ + 'file' => '/var/www/CodeIgniter4/app/Controllers/Home.php', + 'line' => 15, + 'function' => 'f', + 'class' => Home::class, + 'type' => '->', + 'args' => [ + 0 => (object) [ + 'password' => 'secret1', + ], + 1 => (object) [ + 'default' => [ + 'password' => 'secret2', + ], + ], + 2 => [ + 'password' => 'secret3', + ], + 3 => [ + 'default' => ['password' => 'secret4'], + ], + ], + ], + 1 => [ + 'file' => '/var/www/CodeIgniter4/system/CodeIgniter.php', + 'line' => 932, + 'function' => 'index', + 'class' => Home::class, + 'type' => '->', + 'args' => [ + ], + ], + ]; + $keysToMask = ['password']; + $path = ''; + + $newTrace = $maskSensitiveData($trace, $keysToMask, $path); + + $this->assertSame(['password' => '******************'], (array) $newTrace[0]['args'][0]); + $this->assertSame(['password' => '******************'], $newTrace[0]['args'][1]->default); + $this->assertSame(['password' => '******************'], $newTrace[0]['args'][2]); + $this->assertSame(['password' => '******************'], $newTrace[0]['args'][3]['default']); + } + + public function testMaskSensitiveDataTraceDataKey(): void + { + $maskSensitiveData = $this->getPrivateMethodInvoker($this->handler, 'maskSensitiveData'); + + $trace = [ + 0 => [ + 'file' => '/var/www/CodeIgniter4/app/Controllers/Home.php', + 'line' => 15, + 'function' => 'f', + 'class' => Home::class, + 'type' => '->', + 'args' => [ + ], + ], + 1 => [ + 'file' => '/var/www/CodeIgniter4/system/CodeIgniter.php', + 'line' => 932, + 'function' => 'index', + 'class' => Home::class, + 'type' => '->', + 'args' => [ + ], + ], + ]; + $keysToMask = ['file']; + $path = ''; + + $newTrace = $maskSensitiveData($trace, $keysToMask, $path); + + $this->assertSame('/var/www/CodeIgniter4/app/Controllers/Home.php', $newTrace[0]['file']); + } } From ec6835d1b93c06578bd9f746484d40c1b72a1ab6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Jul 2023 06:38:20 +0900 Subject: [PATCH 382/485] fix: declare param/return types --- system/Debug/BaseExceptionHandler.php | 4 +--- system/Debug/Exceptions.php | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php index 0a4bfc8eeb1c..0330c53cf97c 100644 --- a/system/Debug/BaseExceptionHandler.php +++ b/system/Debug/BaseExceptionHandler.php @@ -86,10 +86,8 @@ protected function collectVars(Throwable $exception, int $statusCode): array /** * Mask sensitive data in the trace. - * - * @param array|object $trace */ - protected function maskSensitiveData($trace, array $keysToMask, string $path = '') + protected function maskSensitiveData(array $trace, array $keysToMask, string $path = ''): array { foreach ($trace as $i => $line) { $trace[$i]['args'] = $this->maskData($line['args'], $keysToMask); diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index d185070412a2..8f07681f3a31 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -334,9 +334,9 @@ protected function collectVars(Throwable $exception, int $statusCode): array /** * Mask sensitive data in the trace. * - * @param array|object $trace + * @param array $trace * - * @return array|object + * @return array * * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ From a8b528e789bc07f0fa9626210a6a7a8f9374f017 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 9 Jul 2023 17:12:50 +0900 Subject: [PATCH 383/485] style: break long lines --- system/Config/Factories.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 222df063196a..fa533d9a791b 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -125,7 +125,11 @@ public static function __callStatic(string $component, array $arguments) protected static function locateClass(array $options, string $name): ?string { // Check for low-hanging fruit - if (class_exists($name, false) && self::verifyPreferApp($options, $name) && self::verifyInstanceOf($options, $name)) { + if ( + class_exists($name, false) + && self::verifyPreferApp($options, $name) + && self::verifyInstanceOf($options, $name) + ) { return $name; } @@ -136,7 +140,10 @@ protected static function locateClass(array $options, string $name): ?string : rtrim(APP_NAMESPACE, '\\') . '\\' . $options['path'] . '\\' . $basename; // If an App version was requested then see if it verifies - if ($options['preferApp'] && class_exists($appname) && self::verifyInstanceOf($options, $name)) { + if ( + $options['preferApp'] && class_exists($appname) + && self::verifyInstanceOf($options, $name) + ) { return $appname; } From 85dd31b54817c22c7cceb5c532939a3845691aa1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 10:48:07 +0900 Subject: [PATCH 384/485] refactor: extract isConfig() --- system/Config/Factories.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index fa533d9a791b..16be3abe0e2f 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -116,6 +116,14 @@ public static function __callStatic(string $component, array $arguments) return self::$instances[$options['component']][$class]; } + /** + * Is the component Config? + */ + private static function isConfig(string $component): bool + { + return $component === 'config'; + } + /** * Finds a component class * @@ -135,7 +143,7 @@ class_exists($name, false) // Determine the relative class names we need $basename = self::getBasename($name); - $appname = $options['component'] === 'config' + $appname = self::isConfig($options['component']) ? 'Config\\' . $basename : rtrim(APP_NAMESPACE, '\\') . '\\' . $options['path'] . '\\' . $basename; @@ -194,7 +202,7 @@ protected static function verifyPreferApp(array $options, string $name): bool } // Special case for Config since its App namespace is actually \Config - if ($options['component'] === 'config') { + if (self::isConfig($options['component'])) { return strpos($name, 'Config') === 0; } @@ -233,7 +241,7 @@ public static function getOptions(string $component): array return self::$options[$component]; } - $values = $component === 'config' + $values = self::isConfig($component) // Handle Config as a special case to prevent logic loops ? self::$configOptions // Load values from the best Factory configuration (will include Registrars) From 79d3f351c9459ac464b6c23b49ebbb4f5ecfcb20 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 13:51:52 +0900 Subject: [PATCH 385/485] docs: add doc comments --- system/Config/Factories.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 16be3abe0e2f..a0802bdf9bd2 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -65,6 +65,8 @@ class Factories * A multi-dimensional array with components as * keys to the array of name-indexed instances. * + * [component => [FQCN => instance]] + * * @var array> * @phpstan-var array> */ @@ -118,6 +120,8 @@ public static function __callStatic(string $component, array $arguments) /** * Is the component Config? + * + * @param string $component Lowercase, plural component name */ private static function isConfig(string $component): bool { @@ -231,6 +235,8 @@ protected static function verifyInstanceOf(array $options, string $name): bool * @param string $component Lowercase, plural component name * * @return array + * + * @internal For testing only */ public static function getOptions(string $component): array { @@ -247,6 +253,8 @@ public static function getOptions(string $component): array // Load values from the best Factory configuration (will include Registrars) : config(Factory::class)->{$component} ?? []; + // The setOptions() reset the component. So getOptions() may reset + // the component. return self::setOptions($component, $values); } @@ -254,6 +262,7 @@ public static function getOptions(string $component): array * Normalizes, stores, and returns the configuration for a specific component * * @param string $component Lowercase, plural component name + * @param array $values option values * * @return array The result after applying defaults and normalization */ @@ -305,6 +314,8 @@ public static function reset(?string $component = null) * * @param string $component Lowercase, plural component name * @param string $name The name of the instance + * + * @internal For testing only */ public static function injectMock(string $component, string $name, object $instance) { @@ -321,6 +332,8 @@ public static function injectMock(string $component, string $name, object $insta /** * Gets a basename from a class name, namespaced or not. + * + * @internal For testing only */ public static function getBasename(string $name): string { From d7ed0536b786e473ccb6570592cbfc8bc0b44bad Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 13:52:23 +0900 Subject: [PATCH 386/485] feat: [BC] add Factories::define() to override module classes Except for Config, if FQCN is specified, preferApp is ignored and that class is loaded. --- system/Config/Factories.php | 95 +++++++++++++++++++++--- tests/system/Config/FactoriesTest.php | 103 ++++++++++++++++++++++++-- 2 files changed, 183 insertions(+), 15 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index a0802bdf9bd2..c91d62d75b37 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -14,6 +14,7 @@ use CodeIgniter\Database\ConnectionInterface; use CodeIgniter\Model; use Config\Services; +use InvalidArgumentException; /** * Factories for creating instances. @@ -51,9 +52,11 @@ class Factories ]; /** - * Mapping of class basenames (no namespace) to + * Mapping of classnames (with or without namespace) to * their instances. * + * [component => [name => FQCN]] + * * @var array> * @phpstan-var array> */ @@ -72,6 +75,37 @@ class Factories */ protected static $instances = []; + /** + * Define the class to load. You can *override* the concrete class. + * + * @param string $component Lowercase, plural component name + * @param string $name Classname. The first parameter of Factories magic method + * @param string $classname FQCN to load + * @phpstan-param class-string $classname FQCN to load + */ + public static function define(string $component, string $name, string $classname): void + { + if (isset(self::$basenames[$component][$name])) { + if (self::$basenames[$component][$name] === $classname) { + return; + } + + throw new InvalidArgumentException( + 'Already defined in Factories: ' . $component . ' ' . $name . ' -> ' . self::$basenames[$component][$name] + ); + } + + if (! class_exists($classname)) { + throw new InvalidArgumentException('No such class: ' . $classname); + } + + // Force a configuration to exist for this component. + // Otherwise, getOptions() will reset the component. + self::getOptions($component); + + self::$basenames[$component][$name] = $classname; + } + /** * Loads instances based on the method component name. Either * creates a new instance or returns an existing shared instance. @@ -88,6 +122,12 @@ public static function __callStatic(string $component, array $arguments) $options = array_merge(self::getOptions(strtolower($component)), $options); if (! $options['getShared']) { + if (isset(self::$basenames[$component][$name])) { + $class = self::$basenames[$component][$name]; + + return new $class(...$arguments); + } + if ($class = self::locateClass($options, $name)) { return new $class(...$arguments); } @@ -95,15 +135,39 @@ public static function __callStatic(string $component, array $arguments) return null; } - $basename = self::getBasename($name); - // Check for an existing instance - if (isset(self::$basenames[$options['component']][$basename])) { - $class = self::$basenames[$options['component']][$basename]; + if (isset(self::$basenames[$options['component']][$name])) { + $class = self::$basenames[$options['component']][$name]; // Need to verify if the shared instance matches the request if (self::verifyInstanceOf($options, $class)) { + if (isset(self::$instances[$options['component']][$class])) { + return self::$instances[$options['component']][$class]; + } + self::$instances[$options['component']][$class] = new $class(...$arguments); + return self::$instances[$options['component']][$class]; + + } + } + + // Check for an existing Config instance with basename. + if (self::isConfig($options['component'])) { + $basename = self::getBasename($name); + + if (isset(self::$basenames[$options['component']][$basename])) { + $class = self::$basenames[$options['component']][$basename]; + + // Need to verify if the shared instance matches the request + if (self::verifyInstanceOf($options, $class)) { + if (isset(self::$instances[$options['component']][$class])) { + return self::$instances[$options['component']][$class]; + } + self::$instances[$options['component']][$class] = new $class(...$arguments); + + return self::$instances[$options['component']][$class]; + + } } } @@ -112,8 +176,13 @@ public static function __callStatic(string $component, array $arguments) return null; } - self::$instances[$options['component']][$class] = new $class(...$arguments); - self::$basenames[$options['component']][$basename] = $class; + self::$instances[$options['component']][$class] = new $class(...$arguments); + self::$basenames[$options['component']][$name] = $class; + + // If a short classname is specified, also register FQCN to share the instance. + if (! isset(self::$basenames[$options['component']][$class])) { + self::$basenames[$options['component']][$class] = $class; + } return self::$instances[$options['component']][$class]; } @@ -153,7 +222,9 @@ class_exists($name, false) // If an App version was requested then see if it verifies if ( - $options['preferApp'] && class_exists($appname) + // preferApp is used only for no namespace class or Config class. + (strpos($name, '\\') === false || self::isConfig($options['component'])) + && $options['preferApp'] && class_exists($appname) && self::verifyInstanceOf($options, $name) ) { return $appname; @@ -326,8 +397,12 @@ public static function injectMock(string $component, string $name, object $insta $class = get_class($instance); $basename = self::getBasename($name); - self::$instances[$component][$class] = $instance; - self::$basenames[$component][$basename] = $class; + self::$instances[$component][$class] = $instance; + self::$basenames[$component][$name] = $class; + + if (self::isConfig($component)) { + self::$basenames[$component][$basename] = $class; + } } /** diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index 655329ad95de..dc56c6535ab6 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -12,9 +12,13 @@ namespace CodeIgniter\Config; use CodeIgniter\Test\CIUnitTestCase; +use InvalidArgumentException; use ReflectionClass; use stdClass; +use Tests\Support\Config\TestRegistrar; +use Tests\Support\Models\EntityModel; use Tests\Support\Models\UserModel; +use Tests\Support\View\SampleClass; use Tests\Support\Widgets\OtherWidget; use Tests\Support\Widgets\SomeWidget; @@ -251,7 +255,33 @@ class_alias(SomeWidget::class, $class); $this->assertInstanceOf(SomeWidget::class, $result); } - public function testpreferAppOverridesClassname() + public function testPreferAppOverridesConfigClassname() + { + // Create a config class in App + $file = APPPATH . 'Config/TestRegistrar.php'; + $source = <<<'EOL' + assertInstanceOf('Config\TestRegistrar', $result); + + Factories::setOptions('config', ['preferApp' => false]); + + $result = Factories::config(TestRegistrar::class); + + $this->assertInstanceOf(TestRegistrar::class, $result); + + // Delete the config class in App + unlink($file); + } + + public function testPreferAppIsIgnored() { // Create a fake class in App $class = 'App\Widgets\OtherWidget'; @@ -260,11 +290,74 @@ class_alias(SomeWidget::class, $class); } $result = Factories::widgets(OtherWidget::class); - $this->assertInstanceOf(SomeWidget::class, $result); + $this->assertInstanceOf(OtherWidget::class, $result); + } - Factories::setOptions('widgets', ['preferApp' => false]); + public function testCanLoadTwoCellsWithSameShortName() + { + $cell1 = Factories::cells('\\' . SampleClass::class); + $cell2 = Factories::cells('\\' . \Tests\Support\View\OtherCells\SampleClass::class); - $result = Factories::widgets(OtherWidget::class); - $this->assertInstanceOf(OtherWidget::class, $result); + $this->assertNotSame($cell1, $cell2); + } + + public function testDefineTwice() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Already defined in Factories: models CodeIgniter\Shield\Models\UserModel -> Tests\Support\Models\UserModel' + ); + + Factories::define( + 'models', + 'CodeIgniter\Shield\Models\UserModel', + UserModel::class + ); + Factories::define( + 'models', + 'CodeIgniter\Shield\Models\UserModel', + EntityModel::class + ); + } + + public function testDefineNonExistentClass() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No such class: App\Models\UserModel'); + + Factories::define( + 'models', + 'CodeIgniter\Shield\Models\UserModel', + 'App\Models\UserModel' + ); + } + + public function testDefineAfterLoading() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Already defined in Factories: models Tests\Support\Models\UserModel -> Tests\Support\Models\UserModel' + ); + + model(UserModel::class); + + Factories::define( + 'models', + UserModel::class, + 'App\Models\UserModel' + ); + } + + public function testDefineAndLoad() + { + Factories::define( + 'models', + UserModel::class, + EntityModel::class + ); + + $model = model(UserModel::class); + + $this->assertInstanceOf(EntityModel::class, $model); } } From 510c73fea47b74d2947b98af9e8187d2a23ca9b1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 16:41:03 +0900 Subject: [PATCH 387/485] chore: update psalm-baseline.xml for UndefinedClass --- psalm-baseline.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 41c5c1830d3f..dcef743f607f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + Memcache @@ -104,7 +104,9 @@ - + + + From 8629fdd4154ea4e115c10146c66ffa01f326e1d4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 17:08:12 +0900 Subject: [PATCH 388/485] test: update failed test --- tests/system/Test/FabricatorTest.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 6f92be2aaaad..fdc2d8465a8b 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -11,7 +11,7 @@ namespace CodeIgniter\Test; -use CodeIgniter\Database\ModelFactory; +use CodeIgniter\Config\Factories; use Tests\Support\Models\EntityModel; use Tests\Support\Models\EventModel; use Tests\Support\Models\FabricatorModel; @@ -98,12 +98,16 @@ public function testConstructorUsesProvidedLocale() public function testModelUsesNewInstance() { - // Inject the wrong model for UserModel to show it is ignored by Fabricator + // Inject the wrong model for UserModel $mock = new FabricatorModel(); - ModelFactory::injectMock(UserModel::class, $mock); + Factories::injectMock('models', UserModel::class, $mock); $fabricator = new Fabricator(UserModel::class); - $this->assertInstanceOf(UserModel::class, $fabricator->getModel()); + + // Fabricator gets the instance from Factories, so it is FabricatorModel. + $this->assertInstanceOf(FabricatorModel::class, $fabricator->getModel()); + // But Fabricator creates a new instance. + $this->assertNotSame($mock, $fabricator->getModel()); } public function testGetModelReturnsModel() From 3f23901eee76442a97faf360673ca5fb1fd80eed Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 17:45:26 +0900 Subject: [PATCH 389/485] fix: when injecting Config mock with `Validation`, register the same instance as `Config\Validation` When you call `Factories::injectMock('config', 'Validation', $config)`, but if the production code calls `config(\Config\Validation::class)`, the mock instance will not be used and fail the test: CodeIgniter\Database\ModelFactoryTest::testBasenameReturnsExistingNamespaceInstance --- system/Config/Factories.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index c91d62d75b37..54620e3cf7a9 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -401,7 +401,11 @@ public static function injectMock(string $component, string $name, object $insta self::$basenames[$component][$name] = $class; if (self::isConfig($component)) { - self::$basenames[$component][$basename] = $class; + if ($name !== $basename) { + self::$basenames[$component][$basename] = $class; + } else { + self::$basenames[$component]['Config\\' . $basename] = $class; + } } } From 86ca97bec97d5bd2d6c785868d2d2484ed411d00 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 18:10:58 +0900 Subject: [PATCH 390/485] style: remove empty line --- system/Config/Factories.php | 1 - 1 file changed, 1 deletion(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 54620e3cf7a9..8d8fd148a89b 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -166,7 +166,6 @@ public static function __callStatic(string $component, array $arguments) self::$instances[$options['component']][$class] = new $class(...$arguments); return self::$instances[$options['component']][$class]; - } } } From 74177efa190da86270a4749b2278d693f2d49061 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 18:11:29 +0900 Subject: [PATCH 391/485] test: update failed test --- tests/system/Database/ModelFactoryTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/Database/ModelFactoryTest.php b/tests/system/Database/ModelFactoryTest.php index 4ec47a9b5967..1f69ee865bb5 100644 --- a/tests/system/Database/ModelFactoryTest.php +++ b/tests/system/Database/ModelFactoryTest.php @@ -66,12 +66,12 @@ public function testReset() $this->assertNull(ModelFactory::get('Banana')); } - public function testBasenameReturnsExistingNamespaceInstance() + public function testBasenameDoesNotReturnExistingNamespaceInstance() { ModelFactory::injectMock(UserModel::class, new JobModel()); $basenameModel = ModelFactory::get('UserModel'); - $this->assertInstanceOf(JobModel::class, $basenameModel); + $this->assertInstanceOf(UserModel::class, $basenameModel); } } From 88bbbcf906b5988ea448466896b8db48bd5e6d7d Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jul 2023 18:56:44 +0900 Subject: [PATCH 392/485] fix: preferApp is used with short classname only --- system/Config/Factories.php | 4 ++-- tests/system/Config/FactoriesTest.php | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 8d8fd148a89b..a5e085c1d8aa 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -221,8 +221,8 @@ class_exists($name, false) // If an App version was requested then see if it verifies if ( - // preferApp is used only for no namespace class or Config class. - (strpos($name, '\\') === false || self::isConfig($options['component'])) + // preferApp is used only for no namespace class. + strpos($name, '\\') === false && $options['preferApp'] && class_exists($appname) && self::verifyInstanceOf($options, $name) ) { diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index dc56c6535ab6..1b3a68bbe707 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -255,7 +255,7 @@ class_alias(SomeWidget::class, $class); $this->assertInstanceOf(SomeWidget::class, $result); } - public function testPreferAppOverridesConfigClassname() + public function testShortnameReturnsConfigInApp() { // Create a config class in App $file = APPPATH . 'Config/TestRegistrar.php'; @@ -267,10 +267,30 @@ class TestRegistrar EOL; file_put_contents($file, $source); - $result = Factories::config(TestRegistrar::class); + $result = Factories::config('TestRegistrar'); $this->assertInstanceOf('Config\TestRegistrar', $result); + // Delete the config class in App + unlink($file); + } + + public function testFullClassnameIgnoresPreferApp() + { + // Create a config class in App + $file = APPPATH . 'Config/TestRegistrar.php'; + $source = <<<'EOL' + assertInstanceOf(TestRegistrar::class, $result); + Factories::setOptions('config', ['preferApp' => false]); $result = Factories::config(TestRegistrar::class); From da9ea306a692136945b21dc8c38352e87ed823bb Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 10:35:04 +0900 Subject: [PATCH 393/485] docs: introduce term "class alias" for docs --- system/Config/Factories.php | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index a5e085c1d8aa..14eb11e5483b 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -52,10 +52,14 @@ class Factories ]; /** - * Mapping of classnames (with or without namespace) to - * their instances. + * Mapping of class aliases to their true Full Qualified Class Name (FQCN). * - * [component => [name => FQCN]] + * Class aliases can be: + * - FQCN. E.g., 'App\Lib\SomeLib' + * - short classname. E.g., 'SomeLib' + * - short classname with sub-directories. E.g., 'Sub/SomeLib' + * + * [component => [alias => FQCN]] * * @var array> * @phpstan-var array> @@ -65,6 +69,7 @@ class Factories /** * Store for instances of any component that * has been requested as "shared". + * * A multi-dimensional array with components as * keys to the array of name-indexed instances. * @@ -79,7 +84,7 @@ class Factories * Define the class to load. You can *override* the concrete class. * * @param string $component Lowercase, plural component name - * @param string $name Classname. The first parameter of Factories magic method + * @param string $name Class alias. See the $basenames property. * @param string $classname FQCN to load * @phpstan-param class-string $classname FQCN to load */ @@ -114,7 +119,7 @@ public static function define(string $component, string $name, string $classname */ public static function __callStatic(string $component, array $arguments) { - // First argument is the name, second is options + // First argument is the class alias, second is options $name = trim(array_shift($arguments), '\\ '); $options = array_shift($arguments) ?? []; @@ -200,7 +205,7 @@ private static function isConfig(string $component): bool * Finds a component class * * @param array $options The array of component-specific directives - * @param string $name Class name, namespace optional + * @param string $name Class alias. See the $basenames property. */ protected static function locateClass(array $options, string $name): ?string { @@ -266,7 +271,7 @@ class_exists($name, false) * Verifies that a class & config satisfy the "preferApp" option * * @param array $options The array of component-specific directives - * @param string $name Class name, namespace optional + * @param string $name Class alias. See the $basenames property. */ protected static function verifyPreferApp(array $options, string $name): bool { @@ -287,7 +292,7 @@ protected static function verifyPreferApp(array $options, string $name): bool * Verifies that a class & config satisfy the "instanceOf" option * * @param array $options The array of component-specific directives - * @param string $name Class name, namespace optional + * @param string $name Class alias. See the $basenames property. */ protected static function verifyInstanceOf(array $options, string $name): bool { @@ -383,7 +388,7 @@ public static function reset(?string $component = null) * Helper method for injecting mock instances * * @param string $component Lowercase, plural component name - * @param string $name The name of the instance + * @param string $name Class alias. See the $basenames property. * * @internal For testing only */ From fd4d88c0b29abf285c62b075074876d9e3b86755 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 10:40:18 +0900 Subject: [PATCH 394/485] refactor: rename $basenames to $aliases --- system/Config/Factories.php | 48 ++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 14eb11e5483b..78d386820982 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -64,7 +64,7 @@ class Factories * @var array> * @phpstan-var array> */ - protected static $basenames = []; + protected static $aliases = []; /** * Store for instances of any component that @@ -84,19 +84,19 @@ class Factories * Define the class to load. You can *override* the concrete class. * * @param string $component Lowercase, plural component name - * @param string $name Class alias. See the $basenames property. + * @param string $name Class alias. See the $aliases property. * @param string $classname FQCN to load * @phpstan-param class-string $classname FQCN to load */ public static function define(string $component, string $name, string $classname): void { - if (isset(self::$basenames[$component][$name])) { - if (self::$basenames[$component][$name] === $classname) { + if (isset(self::$aliases[$component][$name])) { + if (self::$aliases[$component][$name] === $classname) { return; } throw new InvalidArgumentException( - 'Already defined in Factories: ' . $component . ' ' . $name . ' -> ' . self::$basenames[$component][$name] + 'Already defined in Factories: ' . $component . ' ' . $name . ' -> ' . self::$aliases[$component][$name] ); } @@ -108,7 +108,7 @@ public static function define(string $component, string $name, string $classname // Otherwise, getOptions() will reset the component. self::getOptions($component); - self::$basenames[$component][$name] = $classname; + self::$aliases[$component][$name] = $classname; } /** @@ -127,8 +127,8 @@ public static function __callStatic(string $component, array $arguments) $options = array_merge(self::getOptions(strtolower($component)), $options); if (! $options['getShared']) { - if (isset(self::$basenames[$component][$name])) { - $class = self::$basenames[$component][$name]; + if (isset(self::$aliases[$component][$name])) { + $class = self::$aliases[$component][$name]; return new $class(...$arguments); } @@ -141,8 +141,8 @@ public static function __callStatic(string $component, array $arguments) } // Check for an existing instance - if (isset(self::$basenames[$options['component']][$name])) { - $class = self::$basenames[$options['component']][$name]; + if (isset(self::$aliases[$options['component']][$name])) { + $class = self::$aliases[$options['component']][$name]; // Need to verify if the shared instance matches the request if (self::verifyInstanceOf($options, $class)) { @@ -160,8 +160,8 @@ public static function __callStatic(string $component, array $arguments) if (self::isConfig($options['component'])) { $basename = self::getBasename($name); - if (isset(self::$basenames[$options['component']][$basename])) { - $class = self::$basenames[$options['component']][$basename]; + if (isset(self::$aliases[$options['component']][$basename])) { + $class = self::$aliases[$options['component']][$basename]; // Need to verify if the shared instance matches the request if (self::verifyInstanceOf($options, $class)) { @@ -181,11 +181,11 @@ public static function __callStatic(string $component, array $arguments) } self::$instances[$options['component']][$class] = new $class(...$arguments); - self::$basenames[$options['component']][$name] = $class; + self::$aliases[$options['component']][$name] = $class; // If a short classname is specified, also register FQCN to share the instance. - if (! isset(self::$basenames[$options['component']][$class])) { - self::$basenames[$options['component']][$class] = $class; + if (! isset(self::$aliases[$options['component']][$class])) { + self::$aliases[$options['component']][$class] = $class; } return self::$instances[$options['component']][$class]; @@ -205,7 +205,7 @@ private static function isConfig(string $component): bool * Finds a component class * * @param array $options The array of component-specific directives - * @param string $name Class alias. See the $basenames property. + * @param string $name Class alias. See the $aliases property. */ protected static function locateClass(array $options, string $name): ?string { @@ -271,7 +271,7 @@ class_exists($name, false) * Verifies that a class & config satisfy the "preferApp" option * * @param array $options The array of component-specific directives - * @param string $name Class alias. See the $basenames property. + * @param string $name Class alias. See the $aliases property. */ protected static function verifyPreferApp(array $options, string $name): bool { @@ -292,7 +292,7 @@ protected static function verifyPreferApp(array $options, string $name): bool * Verifies that a class & config satisfy the "instanceOf" option * * @param array $options The array of component-specific directives - * @param string $name Class alias. See the $basenames property. + * @param string $name Class alias. See the $aliases property. */ protected static function verifyInstanceOf(array $options, string $name): bool { @@ -372,7 +372,7 @@ public static function reset(?string $component = null) if ($component) { unset( static::$options[$component], - static::$basenames[$component], + static::$aliases[$component], static::$instances[$component] ); @@ -380,7 +380,7 @@ public static function reset(?string $component = null) } static::$options = []; - static::$basenames = []; + static::$aliases = []; static::$instances = []; } @@ -388,7 +388,7 @@ public static function reset(?string $component = null) * Helper method for injecting mock instances * * @param string $component Lowercase, plural component name - * @param string $name Class alias. See the $basenames property. + * @param string $name Class alias. See the $aliases property. * * @internal For testing only */ @@ -402,13 +402,13 @@ public static function injectMock(string $component, string $name, object $insta $basename = self::getBasename($name); self::$instances[$component][$class] = $instance; - self::$basenames[$component][$name] = $class; + self::$aliases[$component][$name] = $class; if (self::isConfig($component)) { if ($name !== $basename) { - self::$basenames[$component][$basename] = $class; + self::$aliases[$component][$basename] = $class; } else { - self::$basenames[$component]['Config\\' . $basename] = $class; + self::$aliases[$component]['Config\\' . $basename] = $class; } } } From 0fac66c50337b4912142448d293e7efc5b8c1dba Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 10:42:26 +0900 Subject: [PATCH 395/485] refactor: rename $name to $alias --- system/Config/Factories.php | 90 ++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 78d386820982..727c0bbb5512 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -25,7 +25,7 @@ * instantiation checks. * * @method static BaseConfig|null config(...$arguments) - * @method static Model|null models(string $name, array $options = [], ?ConnectionInterface &$conn = null) + * @method static Model|null models(string $alias, array $options = [], ?ConnectionInterface &$conn = null) */ class Factories { @@ -84,19 +84,19 @@ class Factories * Define the class to load. You can *override* the concrete class. * * @param string $component Lowercase, plural component name - * @param string $name Class alias. See the $aliases property. + * @param string $alias Class alias. See the $aliases property. * @param string $classname FQCN to load * @phpstan-param class-string $classname FQCN to load */ - public static function define(string $component, string $name, string $classname): void + public static function define(string $component, string $alias, string $classname): void { - if (isset(self::$aliases[$component][$name])) { - if (self::$aliases[$component][$name] === $classname) { + if (isset(self::$aliases[$component][$alias])) { + if (self::$aliases[$component][$alias] === $classname) { return; } throw new InvalidArgumentException( - 'Already defined in Factories: ' . $component . ' ' . $name . ' -> ' . self::$aliases[$component][$name] + 'Already defined in Factories: ' . $component . ' ' . $alias . ' -> ' . self::$aliases[$component][$alias] ); } @@ -108,7 +108,7 @@ public static function define(string $component, string $name, string $classname // Otherwise, getOptions() will reset the component. self::getOptions($component); - self::$aliases[$component][$name] = $classname; + self::$aliases[$component][$alias] = $classname; } /** @@ -120,20 +120,20 @@ public static function define(string $component, string $name, string $classname public static function __callStatic(string $component, array $arguments) { // First argument is the class alias, second is options - $name = trim(array_shift($arguments), '\\ '); + $alias = trim(array_shift($arguments), '\\ '); $options = array_shift($arguments) ?? []; // Determine the component-specific options $options = array_merge(self::getOptions(strtolower($component)), $options); if (! $options['getShared']) { - if (isset(self::$aliases[$component][$name])) { - $class = self::$aliases[$component][$name]; + if (isset(self::$aliases[$component][$alias])) { + $class = self::$aliases[$component][$alias]; return new $class(...$arguments); } - if ($class = self::locateClass($options, $name)) { + if ($class = self::locateClass($options, $alias)) { return new $class(...$arguments); } @@ -141,8 +141,8 @@ public static function __callStatic(string $component, array $arguments) } // Check for an existing instance - if (isset(self::$aliases[$options['component']][$name])) { - $class = self::$aliases[$options['component']][$name]; + if (isset(self::$aliases[$options['component']][$alias])) { + $class = self::$aliases[$options['component']][$alias]; // Need to verify if the shared instance matches the request if (self::verifyInstanceOf($options, $class)) { @@ -158,7 +158,7 @@ public static function __callStatic(string $component, array $arguments) // Check for an existing Config instance with basename. if (self::isConfig($options['component'])) { - $basename = self::getBasename($name); + $basename = self::getBasename($alias); if (isset(self::$aliases[$options['component']][$basename])) { $class = self::$aliases[$options['component']][$basename]; @@ -176,12 +176,12 @@ public static function __callStatic(string $component, array $arguments) } // Try to locate the class - if (! $class = self::locateClass($options, $name)) { + if (! $class = self::locateClass($options, $alias)) { return null; } self::$instances[$options['component']][$class] = new $class(...$arguments); - self::$aliases[$options['component']][$name] = $class; + self::$aliases[$options['component']][$alias] = $class; // If a short classname is specified, also register FQCN to share the instance. if (! isset(self::$aliases[$options['component']][$class])) { @@ -205,21 +205,21 @@ private static function isConfig(string $component): bool * Finds a component class * * @param array $options The array of component-specific directives - * @param string $name Class alias. See the $aliases property. + * @param string $alias Class alias. See the $aliases property. */ - protected static function locateClass(array $options, string $name): ?string + protected static function locateClass(array $options, string $alias): ?string { // Check for low-hanging fruit if ( - class_exists($name, false) - && self::verifyPreferApp($options, $name) - && self::verifyInstanceOf($options, $name) + class_exists($alias, false) + && self::verifyPreferApp($options, $alias) + && self::verifyInstanceOf($options, $alias) ) { - return $name; + return $alias; } // Determine the relative class names we need - $basename = self::getBasename($name); + $basename = self::getBasename($alias); $appname = self::isConfig($options['component']) ? 'Config\\' . $basename : rtrim(APP_NAMESPACE, '\\') . '\\' . $options['path'] . '\\' . $basename; @@ -227,31 +227,31 @@ class_exists($name, false) // If an App version was requested then see if it verifies if ( // preferApp is used only for no namespace class. - strpos($name, '\\') === false + strpos($alias, '\\') === false && $options['preferApp'] && class_exists($appname) - && self::verifyInstanceOf($options, $name) + && self::verifyInstanceOf($options, $alias) ) { return $appname; } // If we have ruled out an App version and the class exists then try it - if (class_exists($name) && self::verifyInstanceOf($options, $name)) { - return $name; + if (class_exists($alias) && self::verifyInstanceOf($options, $alias)) { + return $alias; } // Have to do this the hard way... $locator = Services::locator(); // Check if the class was namespaced - if (strpos($name, '\\') !== false) { - if (! $file = $locator->locateFile($name, $options['path'])) { + if (strpos($alias, '\\') !== false) { + if (! $file = $locator->locateFile($alias, $options['path'])) { return null; } $files = [$file]; } // No namespace? Search for it // Check all namespaces, prioritizing App and modules - elseif (! $files = $locator->search($options['path'] . DIRECTORY_SEPARATOR . $name)) { + elseif (! $files = $locator->search($options['path'] . DIRECTORY_SEPARATOR . $alias)) { return null; } @@ -271,9 +271,9 @@ class_exists($name, false) * Verifies that a class & config satisfy the "preferApp" option * * @param array $options The array of component-specific directives - * @param string $name Class alias. See the $aliases property. + * @param string $alias Class alias. See the $aliases property. */ - protected static function verifyPreferApp(array $options, string $name): bool + protected static function verifyPreferApp(array $options, string $alias): bool { // Anything without that restriction passes if (! $options['preferApp']) { @@ -282,26 +282,26 @@ protected static function verifyPreferApp(array $options, string $name): bool // Special case for Config since its App namespace is actually \Config if (self::isConfig($options['component'])) { - return strpos($name, 'Config') === 0; + return strpos($alias, 'Config') === 0; } - return strpos($name, APP_NAMESPACE) === 0; + return strpos($alias, APP_NAMESPACE) === 0; } /** * Verifies that a class & config satisfy the "instanceOf" option * * @param array $options The array of component-specific directives - * @param string $name Class alias. See the $aliases property. + * @param string $alias Class alias. See the $aliases property. */ - protected static function verifyInstanceOf(array $options, string $name): bool + protected static function verifyInstanceOf(array $options, string $alias): bool { // Anything without that restriction passes if (! $options['instanceOf']) { return true; } - return is_a($name, $options['instanceOf'], true); + return is_a($alias, $options['instanceOf'], true); } /** @@ -388,24 +388,24 @@ public static function reset(?string $component = null) * Helper method for injecting mock instances * * @param string $component Lowercase, plural component name - * @param string $name Class alias. See the $aliases property. + * @param string $alias Class alias. See the $aliases property. * * @internal For testing only */ - public static function injectMock(string $component, string $name, object $instance) + public static function injectMock(string $component, string $alias, object $instance) { // Force a configuration to exist for this component $component = strtolower($component); self::getOptions($component); $class = get_class($instance); - $basename = self::getBasename($name); + $basename = self::getBasename($alias); self::$instances[$component][$class] = $instance; - self::$aliases[$component][$name] = $class; + self::$aliases[$component][$alias] = $class; if (self::isConfig($component)) { - if ($name !== $basename) { + if ($alias !== $basename) { self::$aliases[$component][$basename] = $class; } else { self::$aliases[$component]['Config\\' . $basename] = $class; @@ -418,13 +418,13 @@ public static function injectMock(string $component, string $name, object $insta * * @internal For testing only */ - public static function getBasename(string $name): string + public static function getBasename(string $alias): string { // Determine the basename - if ($basename = strrchr($name, '\\')) { + if ($basename = strrchr($alias, '\\')) { return substr($basename, 1); } - return $name; + return $alias; } } From d385e98ef1f3f9e3d32d87e48ea1e3d7aabebab9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 10:47:30 +0900 Subject: [PATCH 396/485] docs: improve doc comments --- system/Config/Factories.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 727c0bbb5512..e48307920fbc 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -85,8 +85,8 @@ class Factories * * @param string $component Lowercase, plural component name * @param string $alias Class alias. See the $aliases property. - * @param string $classname FQCN to load - * @phpstan-param class-string $classname FQCN to load + * @param string $classname FQCN to be loaded + * @phpstan-param class-string $classname FQCN to be loaded */ public static function define(string $component, string $alias, string $classname): void { From 88803513cce5e039c95ff5d20b8345136250bb04 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 10:54:42 +0900 Subject: [PATCH 397/485] refactor: extract isNamespaced() --- system/Config/Factories.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index e48307920fbc..bcb249ba387b 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -242,8 +242,8 @@ class_exists($alias, false) // Have to do this the hard way... $locator = Services::locator(); - // Check if the class was namespaced - if (strpos($alias, '\\') !== false) { + // Check if the class alias was namespaced + if (self::isNamespaced($alias)) { if (! $file = $locator->locateFile($alias, $options['path'])) { return null; } @@ -267,6 +267,16 @@ class_exists($alias, false) return null; } + /** + * Is the class alias namespaced or not? + * + * @param string $alias Class alias. See the $aliases property. + */ + private static function isNamespaced(string $alias): bool + { + return strpos($alias, '\\') !== false; + } + /** * Verifies that a class & config satisfy the "preferApp" option * @@ -414,7 +424,7 @@ public static function injectMock(string $component, string $alias, object $inst } /** - * Gets a basename from a class name, namespaced or not. + * Gets a basename from a class alias, namespaced or not. * * @internal For testing only */ From a1fa03f642fbd09ecbe403dc67be95397998f8fb Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 11:04:59 +0900 Subject: [PATCH 398/485] docs: add @testTag --- system/Config/Factories.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index bcb249ba387b..7430ecdd0dad 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -322,6 +322,7 @@ protected static function verifyInstanceOf(array $options, string $alias): bool * @return array * * @internal For testing only + * @testTag */ public static function getOptions(string $component): array { @@ -401,6 +402,7 @@ public static function reset(?string $component = null) * @param string $alias Class alias. See the $aliases property. * * @internal For testing only + * @testTag */ public static function injectMock(string $component, string $alias, object $instance) { @@ -427,6 +429,7 @@ public static function injectMock(string $component, string $alias, object $inst * Gets a basename from a class alias, namespaced or not. * * @internal For testing only + * @testTag */ public static function getBasename(string $alias): string { From 5b05bf93f62a75ae9ad191f8516352ed67f8d0bc Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 16:20:48 +0900 Subject: [PATCH 399/485] refactor: use isNamespaced() --- system/Config/Factories.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 7430ecdd0dad..93a2193a12bf 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -410,17 +410,16 @@ public static function injectMock(string $component, string $alias, object $inst $component = strtolower($component); self::getOptions($component); - $class = get_class($instance); - $basename = self::getBasename($alias); + $class = get_class($instance); self::$instances[$component][$class] = $instance; self::$aliases[$component][$alias] = $class; if (self::isConfig($component)) { - if ($alias !== $basename) { - self::$aliases[$component][$basename] = $class; + if (self::isNamespaced($alias)) { + self::$aliases[$component][self::getBasename($alias)] = $class; } else { - self::$aliases[$component]['Config\\' . $basename] = $class; + self::$aliases[$component]['Config\\' . $alias] = $class; } } } From b48277466480b2252480fbec3de0ca9574f64948 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 16:27:38 +0900 Subject: [PATCH 400/485] refactor: use isNamespaced() --- system/Config/Factories.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 93a2193a12bf..f75c87d59cd9 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -226,8 +226,8 @@ class_exists($alias, false) // If an App version was requested then see if it verifies if ( - // preferApp is used only for no namespace class. - strpos($alias, '\\') === false + // preferApp is used only for no namespaced class. + ! self::isNamespaced($alias) && $options['preferApp'] && class_exists($appname) && self::verifyInstanceOf($options, $alias) ) { From a3c768ab4b37a0082344ad4121ab260b4bea3bf7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Jul 2023 09:10:45 +0900 Subject: [PATCH 401/485] test: add test --- tests/system/Config/FactoriesTest.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index 1b3a68bbe707..ffbf8b7b8a83 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -321,7 +321,7 @@ public function testCanLoadTwoCellsWithSameShortName() $this->assertNotSame($cell1, $cell2); } - public function testDefineTwice() + public function testDefineSameAliasTwiceWithDifferentClasses() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( @@ -340,6 +340,24 @@ public function testDefineTwice() ); } + public function testDefineSameAliasAndSameClassTwice() + { + Factories::define( + 'models', + 'CodeIgniter\Shield\Models\UserModel', + UserModel::class + ); + Factories::define( + 'models', + 'CodeIgniter\Shield\Models\UserModel', + UserModel::class + ); + + $model = model('CodeIgniter\Shield\Models\UserModel'); + + $this->assertInstanceOf(UserModel::class, $model); + } + public function testDefineNonExistentClass() { $this->expectException(InvalidArgumentException::class); From e3c223bbd96b2f3697e73c32bb71d78ac6c3f0de Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Jul 2023 09:20:04 +0900 Subject: [PATCH 402/485] docs: update comments --- system/Config/Factories.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index f75c87d59cd9..94fa7cf71330 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -140,15 +140,17 @@ public static function __callStatic(string $component, array $arguments) return null; } - // Check for an existing instance + // Check for an existing definition if (isset(self::$aliases[$options['component']][$alias])) { $class = self::$aliases[$options['component']][$alias]; // Need to verify if the shared instance matches the request if (self::verifyInstanceOf($options, $class)) { + // Check for an existing instance if (isset(self::$instances[$options['component']][$class])) { return self::$instances[$options['component']][$class]; } + self::$instances[$options['component']][$class] = new $class(...$arguments); return self::$instances[$options['component']][$class]; @@ -156,7 +158,7 @@ public static function __callStatic(string $component, array $arguments) } } - // Check for an existing Config instance with basename. + // Check for an existing Config definition with basename. if (self::isConfig($options['component'])) { $basename = self::getBasename($alias); @@ -165,9 +167,11 @@ public static function __callStatic(string $component, array $arguments) // Need to verify if the shared instance matches the request if (self::verifyInstanceOf($options, $class)) { + // Check for an existing instance if (isset(self::$instances[$options['component']][$class])) { return self::$instances[$options['component']][$class]; } + self::$instances[$options['component']][$class] = new $class(...$arguments); return self::$instances[$options['component']][$class]; From 244c5fbe7b76bce549e868c588f7fa9195d3268f Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Jul 2023 09:34:34 +0900 Subject: [PATCH 403/485] refactor: extract getDefinedInstance() --- system/Config/Factories.php | 61 +++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 94fa7cf71330..e122a650d366 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -141,41 +141,18 @@ public static function __callStatic(string $component, array $arguments) } // Check for an existing definition - if (isset(self::$aliases[$options['component']][$alias])) { - $class = self::$aliases[$options['component']][$alias]; - - // Need to verify if the shared instance matches the request - if (self::verifyInstanceOf($options, $class)) { - // Check for an existing instance - if (isset(self::$instances[$options['component']][$class])) { - return self::$instances[$options['component']][$class]; - } - - self::$instances[$options['component']][$class] = new $class(...$arguments); - - return self::$instances[$options['component']][$class]; - - } + $instance = self::getDefinedInstance($options, $alias, $arguments); + if ($instance !== null) { + return $instance; } // Check for an existing Config definition with basename. if (self::isConfig($options['component'])) { $basename = self::getBasename($alias); - if (isset(self::$aliases[$options['component']][$basename])) { - $class = self::$aliases[$options['component']][$basename]; - - // Need to verify if the shared instance matches the request - if (self::verifyInstanceOf($options, $class)) { - // Check for an existing instance - if (isset(self::$instances[$options['component']][$class])) { - return self::$instances[$options['component']][$class]; - } - - self::$instances[$options['component']][$class] = new $class(...$arguments); - - return self::$instances[$options['component']][$class]; - } + $instance = self::getDefinedInstance($options, $basename, $arguments); + if ($instance !== null) { + return $instance; } } @@ -195,6 +172,32 @@ public static function __callStatic(string $component, array $arguments) return self::$instances[$options['component']][$class]; } + /** + * Gets the defined instance. If not exists, creates new one. + * + * @return object|null + */ + private static function getDefinedInstance(array $options, string $alias, array $arguments) + { + if (isset(self::$aliases[$options['component']][$alias])) { + $class = self::$aliases[$options['component']][$alias]; + + // Need to verify if the shared instance matches the request + if (self::verifyInstanceOf($options, $class)) { + // Check for an existing instance + if (isset(self::$instances[$options['component']][$class])) { + return self::$instances[$options['component']][$class]; + } + + self::$instances[$options['component']][$class] = new $class(...$arguments); + + return self::$instances[$options['component']][$class]; + } + } + + return null; + } + /** * Is the component Config? * From 340a930dcfcd088a6b094747a7b44e724c5dc855 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Jul 2023 09:42:14 +0900 Subject: [PATCH 404/485] fix: remove logic for preferApp for no namespaced Config classname preferApp should work only for no namespaced (Config) classname. --- system/Config/Factories.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index e122a650d366..a33079bec4d5 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -146,16 +146,6 @@ public static function __callStatic(string $component, array $arguments) return $instance; } - // Check for an existing Config definition with basename. - if (self::isConfig($options['component'])) { - $basename = self::getBasename($alias); - - $instance = self::getDefinedInstance($options, $basename, $arguments); - if ($instance !== null) { - return $instance; - } - } - // Try to locate the class if (! $class = self::locateClass($options, $alias)) { return null; From a13c53d5c1ed72544d787df3978196cdaf44ea6c Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Jul 2023 10:20:59 +0900 Subject: [PATCH 405/485] docs: update existing description --- user_guide_src/source/concepts/factories.rst | 48 +++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/user_guide_src/source/concepts/factories.rst b/user_guide_src/source/concepts/factories.rst index b3ec0a064176..0c97052c9a4d 100644 --- a/user_guide_src/source/concepts/factories.rst +++ b/user_guide_src/source/concepts/factories.rst @@ -54,9 +54,18 @@ by using the magic static method of the Factories class, ``Factories::models()`` The static method name is called *component*. -By default, Factories first searches in the ``App`` namespace for the path corresponding to the magic static method name. +.. _factories-passing-classname-without-namespace: + +Passing Classname without Namespace +----------------------------------- + +If you pass a classname without a namespace, Factories first searches in the +``App`` namespace for the path corresponding to the magic static method name. ``Factories::models()`` searches the **app/Models** directory. +Passing Short Classname +^^^^^^^^^^^^^^^^^^^^^^^ + In the following code, if you have ``App\Models\UserModel``, the instance will be returned: .. literalinclude:: factories/001.php @@ -68,31 +77,35 @@ you get back the instance as before: .. literalinclude:: factories/003.php -preferApp option ----------------- +Passing Short Classname with Sub-directories +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -You could also request a specific class: +If you want to load a class in sub directories, you use the ``/`` as a separator. +The following code loads **app/Libraries/Sub/SubLib.php** if it exists: -.. literalinclude:: factories/002.php +.. literalinclude:: factories/013.php :lines: 2- -If you have only ``Blog\Models\UserModel``, the instance will be returned. -But if you have both ``App\Models\UserModel`` and ``Blog\Models\UserModel``, -the instance of ``App\Models\UserModel`` will be returned. +Passing Full Qualified Classname +-------------------------------- -If you want to get ``Blog\Models\UserModel``, you need to disable the option ``preferApp``: +You could also request a full qualified classname: -.. literalinclude:: factories/010.php +.. literalinclude:: factories/002.php :lines: 2- -Loading a Class in Sub-directories -================================== +It returns the instance of ``Blog\Models\UserModel`` if it exists. -If you want to load a class in sub directories, you use the ``/`` as a separator. -The following code loads **app/Libraries/Sub/SubLib.php**: +.. note:: Prior to v4.4.0, when you requested a full qualified classname, + if you had only ``Blog\Models\UserModel``, the instance would be returned. + But if you had both ``App\Models\UserModel`` and ``Blog\Models\UserModel``, + the instance of ``App\Models\UserModel`` would be returned. -.. literalinclude:: factories/013.php - :lines: 2- + If you wanted to get ``Blog\Models\UserModel``, you needed to disable the + option ``preferApp``: + + .. literalinclude:: factories/010.php + :lines: 2- Convenience Functions ********************* @@ -154,6 +167,9 @@ preferApp boolean Whether a class with the same basename in the App name overrides other explicit class requests. ========== ============== ============================================================ =================================================== +.. note:: Since v4.4.0, ``preferApp`` works only when you request + :ref:`a classname without a namespace `. + Factories Behavior ****************** From 886fa8703a7be8312497be1cdaadbe78b9125546 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Jul 2023 10:44:28 +0900 Subject: [PATCH 406/485] docs: add about Factories::define() --- user_guide_src/source/concepts/factories.rst | 23 +++++++++++++++++++ .../source/concepts/factories/014.php | 3 +++ .../source/concepts/factories/015.php | 3 +++ 3 files changed, 29 insertions(+) create mode 100644 user_guide_src/source/concepts/factories/014.php create mode 100644 user_guide_src/source/concepts/factories/015.php diff --git a/user_guide_src/source/concepts/factories.rst b/user_guide_src/source/concepts/factories.rst index 0c97052c9a4d..881f0fa1b1f1 100644 --- a/user_guide_src/source/concepts/factories.rst +++ b/user_guide_src/source/concepts/factories.rst @@ -128,6 +128,29 @@ The second function, :php:func:`model()` returns a new instance of a Model class .. literalinclude:: factories/009.php +.. _factories-defining-classname-to-be-loaded: + +Defining Classname to be Loaded +******************************* + +.. versionadded:: 4.4.0 + +You could define a classname to be loaded before loading the class with +the ``Factories::define()`` method: + +.. literalinclude:: factories/014.php + :lines: 2- + +The first parameter is a component. The second parameter is a class alias +(the first parameter to Factories magic static method), and the third parameter +is the true full qualified classname to be loaded. + +After that, if you load ``Myth\Auth\Models\UserModel`` with Factories, the +``App\Models\UserModel`` instance will be returned: + +.. literalinclude:: factories/015.php + :lines: 2- + Factory Parameters ****************** diff --git a/user_guide_src/source/concepts/factories/014.php b/user_guide_src/source/concepts/factories/014.php new file mode 100644 index 000000000000..992b0f7b2236 --- /dev/null +++ b/user_guide_src/source/concepts/factories/014.php @@ -0,0 +1,3 @@ + Date: Wed, 26 Jul 2023 11:28:42 +0900 Subject: [PATCH 407/485] docs: add changelog and upgrade guide --- user_guide_src/source/changelogs/v4.4.0.rst | 31 +++++++++++++++++++ .../source/installation/upgrade_440.rst | 15 +++++++++ 2 files changed, 46 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 475d08e9eaea..6eb96418c6ae 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -29,6 +29,35 @@ or more was specified. See :ref:`upgrade-440-uri-setsegment`. The next segment (``+1``) of the current last segment can be set as before. +.. _v440-factories: + +Factories +--------- + +Passing Fully Qualified Classname +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Now ``preferApp`` works only when you request +:ref:`a classname without a namespace `. + +For example, when you call ``model(\Myth\Auth\Models\UserModel::class)`` or +``model('Myth\Auth\Models\UserModel')``: + + - before: + + - returns ``App\Models\UserModel`` if exists and ``preferApp`` is true (default) + - returns ``Myth\Auth\Models\UserModel`` if exists and ``preferApp`` is false + + - after: + + - returns ``Myth\Auth\Models\UserModel`` even if ``preferApp`` is true (default) + - returns ``App\Models\UserModel`` if you define ``Factories::define('models', 'Myth\Auth\Models\UserModel', 'App\Models\UserModel')`` before calling the ``model()`` + +Property Name +^^^^^^^^^^^^^ + +The property ``Factories::$basenames`` has been renamed to ``$aliases``. + .. _v440-interface-changes: Interface Changes @@ -164,6 +193,8 @@ Others - **RedirectException:** can also take an object that implements ResponseInterface as its first argument. - **RedirectException:** implements ResponsableInterface. - **DebugBar:** Now :ref:`view-routes` are displayed in *DEFINED ROUTES* on the *Routes* tab. +- **Factories:** You can now define the classname that will actually be loaded. + See :ref:`factories-defining-classname-to-be-loaded`. Message Changes *************** diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index a64e35813a4a..3fc02a179330 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -73,6 +73,21 @@ This bug was fixed and now URIs for underscores (**foo_bar**) is not accessible. If you have links to URIs for underscores (**foo_bar**), update them with URIs for dashes (**foo-bar**). +When Passing Fully Qualified Classnames to Factories +==================================================== + +The behavior of passing fully qualified classnames to Factories has been changed. +See :ref:`ChangeLog ` for details. + +If you have code like ``model('\Myth\Auth\Models\UserModel::class')`` or +``model('Myth\Auth\Models\UserModel')`` (the code may be in the third-party packages), +and you expect to load your ``App\Models\UserModel``, you need to define the +classname to be loaded before the first loading of that class:: + + Factories::define('models', 'Myth\Auth\Models\UserModel', 'App\Models\UserModel'); + +See :ref:`factories-defining-classname-to-be-loaded` for details. + Interface Changes ================= From c48b5239b4c6ace84bf754ca134255a79180d057 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Jul 2023 11:29:54 +0900 Subject: [PATCH 408/485] docs: fix typo --- 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 a33079bec4d5..67a31728d65c 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -52,7 +52,7 @@ class Factories ]; /** - * Mapping of class aliases to their true Full Qualified Class Name (FQCN). + * Mapping of class aliases to their true Fully Qualified Class Name (FQCN). * * Class aliases can be: * - FQCN. E.g., 'App\Lib\SomeLib' From 50b8d7f32cac2616b29e48feb081317dd0d044a2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Jul 2023 11:45:31 +0900 Subject: [PATCH 409/485] docs: group items --- user_guide_src/source/changelogs/v4.4.0.rst | 30 +++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 475d08e9eaea..19fe6bc29b3c 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -150,19 +150,20 @@ Others the ``Content-Disposition: inline`` header to display the file in the browser. See :ref:`open-file-in-browser` for details. - **View:** Added optional 2nd parameter ``$saveData`` on ``renderSection()`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. -- **Auto Routing (Improved)**: Now you can route to Modules. - See :ref:`auto-routing-improved-module-routing` for details. -- **Auto Routing (Improved):** If a controller is found that corresponds to a URI - segment and that controller does not have a method defined for the URI segment, - the default method will now be executed. This addition allows for more flexible - handling of URIs in auto routing. - See :ref:`controller-default-method-fallback` for details. +- **Auto Routing (Improved)**: + - Now you can route to Modules. See :ref:`auto-routing-improved-module-routing` + for details. + - If a controller is found that corresponds to a URI segment and that controller + does not have a method defined for the URI segment, the default method will + now be executed. This addition allows for more flexible handling of URIs in + auto routing. See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. - **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with headings. See :ref:`table-sync-rows-with-headings` for details. - **Error Handling:** Now you can use :ref:`custom-exception-handlers`. -- **RedirectException:** can also take an object that implements ResponseInterface as its first argument. -- **RedirectException:** implements ResponsableInterface. +- **RedirectException:** + - It can also take an object that implements ``ResponseInterface`` as its first argument. + - It implements ``ResponsableInterface``. - **DebugBar:** Now :ref:`view-routes` are displayed in *DEFINED ROUTES* on the *Routes* tab. Message Changes @@ -174,11 +175,12 @@ Changes ******* - **Images:** The default quality for WebP in ``GDHandler`` has been changed from 80 to 90. -- **Config:** The deprecated Cookie items in **app/Config/App.php** has been removed. -- **Config:** The deprecated Session items in **app/Config/App.php** has been removed. -- **Config:** The deprecated CSRF items in **app/Config/App.php** has been removed. -- **Config:** Routing settings have been moved to **app/Config/Routing.php** config file. - See :ref:`Upgrading Guide `. +- **Config:** + - The deprecated Cookie items in **app/Config/App.php** has been removed. + - The deprecated Session items in **app/Config/App.php** has been removed. + - The deprecated CSRF items in **app/Config/App.php** has been removed. + - Routing settings have been moved to **app/Config/Routing.php** config file. + See :ref:`Upgrading Guide `. - **DownloadResponse:** When generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. - **Autoloader:** Before v4.4.0, CodeIgniter autoloader did not allow special characters that are illegal in filenames on certain operating systems. From b5170d46a631b945a38168075fb4564737d995d3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Jul 2023 11:46:09 +0900 Subject: [PATCH 410/485] docs: capitalize first letters --- user_guide_src/source/installation/upgrade_440.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index a64e35813a4a..62fe4214775c 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -47,7 +47,7 @@ If your code depends on this bug, fix the segment number. .. literalinclude:: upgrade_440/002.php :lines: 2- -When you extend Exceptions +When You Extend Exceptions ========================== If you are extending ``CodeIgniter\Debug\Exceptions`` and have not overridden From 89d6f3788741440b0b2035527bb883b957810a93 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 28 Jul 2023 11:21:54 +0900 Subject: [PATCH 411/485] docs: update existing descriptions --- user_guide_src/source/general/configuration.rst | 5 +++-- user_guide_src/source/general/modules.rst | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/user_guide_src/source/general/configuration.rst b/user_guide_src/source/general/configuration.rst index 2e666c8245c8..e51ab1e8fa40 100644 --- a/user_guide_src/source/general/configuration.rst +++ b/user_guide_src/source/general/configuration.rst @@ -47,9 +47,10 @@ All of the configuration files that ship with CodeIgniter are namespaced with ``Config``. Using this namespace in your application will provide the best performance since it knows exactly where to find the files. -.. note:: ``config()`` finds the file in **app/Config/** when there is a class with the same shortname, +.. note:: Prior to v4.4.0, ``config()`` finds the file in **app/Config/** when there + is a class with the same shortname, even if you specify a fully qualified class name like ``config(\Acme\Blog\Config\Blog::class)``. - This is because ``config()`` is a wrapper for the ``Factories`` class which uses ``preferApp`` by default. See :ref:`factories-loading-class` for more information. + This behavior has been fixed in v4.4.0, and returns the specified instance. Getting a Config Property ========================= diff --git a/user_guide_src/source/general/modules.rst b/user_guide_src/source/general/modules.rst index ae79f5431779..92093d5bd5c1 100644 --- a/user_guide_src/source/general/modules.rst +++ b/user_guide_src/source/general/modules.rst @@ -189,14 +189,15 @@ with the ``new`` command: .. literalinclude:: modules/008.php -Config files are automatically discovered whenever using the :php:func:`config()` function that is always available. +Config files are automatically discovered whenever using the :php:func:`config()` function that is always available, and you pass a short classname to it. .. note:: We don't recommend you use the same short classname in modules. - Modules that need to override or add to known configurations in **app/Config/** should use :ref:`registrars`. + Modules that need to override or add to known configurations in **app/Config/** should use :ref:`Implicit Registrars `. -.. note:: ``config()`` finds the file in **app/Config/** when there is a class with the same shortname, +.. note:: Prior to v4.4.0, ``config()`` finds the file in **app/Config/** when there + is a class with the same shortname, even if you specify a fully qualified class name like ``config(\Acme\Blog\Config\Blog::class)``. - This is because ``config()`` is a wrapper for the ``Factories`` class which uses ``preferApp`` by default. See :ref:`factories-loading-class` for more information. + This behavior has been fixed in v4.4.0, and returns the specified instance. Migrations ========== From 912480aa191e7e0df873a897eae0db3efacc0828 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 28 Jul 2023 11:35:16 +0900 Subject: [PATCH 412/485] style: remove emtpy line Fix due to changes in the coding standard. --- system/Router/DefinedRouteCollector.php | 1 - 1 file changed, 1 deletion(-) diff --git a/system/Router/DefinedRouteCollector.php b/system/Router/DefinedRouteCollector.php index f77383fa6cef..9d211415a8a0 100644 --- a/system/Router/DefinedRouteCollector.php +++ b/system/Router/DefinedRouteCollector.php @@ -49,7 +49,6 @@ public function collect(): Generator foreach ($routes as $route => $handler) { if (is_string($handler) || $handler instanceof Closure) { - if ($handler instanceof Closure) { $view = $this->routeCollection->getRoutesOptions($route, $method)['view'] ?? false; From f075b7cd1323247b2861fb831145e6e260855bc9 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sat, 29 Jul 2023 21:34:06 +0800 Subject: [PATCH 413/485] Return signatures of Autoloader's loaders should be void --- phpstan-baseline.php | 10 ---- system/Autoloader/Autoloader.php | 17 +++---- tests/system/Autoloader/AutoloaderTest.php | 53 +++++++++++++-------- user_guide_src/source/changelogs/v4.4.0.rst | 8 ++++ 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index cb13086b8253..53b9554dbb78 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -1,16 +1,6 @@ '#^Parameter \\#1 \\$callback of function spl_autoload_register expects \\(callable\\(string\\)\\: void\\)\\|null, array\\{\\$this\\(CodeIgniter\\\\Autoloader\\\\Autoloader\\), \'loadClass\'\\} given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Autoloader/Autoloader.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$callback of function spl_autoload_register expects \\(callable\\(string\\)\\: void\\)\\|null, array\\{\\$this\\(CodeIgniter\\\\Autoloader\\\\Autoloader\\), \'loadClassmap\'\\} given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Autoloader/Autoloader.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:chunk\\(\\) has parameter \\$userFunc with no signature specified for Closure\\.$#', 'count' => 1, diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 653950bb311d..dc0b72cd6122 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -240,33 +240,30 @@ public function removeNamespace(string $namespace) /** * Load a class using available class mapping. * - * @return false|string + * @internal For `spl_autoload_register` use. */ - public function loadClassmap(string $class) + public function loadClassmap(string $class): void { $file = $this->classmap[$class] ?? ''; if (is_string($file) && $file !== '') { - return $this->includeFile($file); + $this->includeFile($file); } - - return false; } /** * Loads the class file for a given class name. * - * @param string $class The fully qualified class name. + * @internal For `spl_autoload_register` use. * - * @return false|string The mapped file on success, or boolean false - * on failure. + * @param string $class The fully qualified class name. */ - public function loadClass(string $class) + public function loadClass(string $class): void { $class = trim($class, '\\'); $class = str_ireplace('.php', '', $class); - return $this->loadInNamespace($class); + $this->loadInNamespace($class); } /** diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index a116bc9515d6..d877d66e406d 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -12,12 +12,15 @@ namespace CodeIgniter\Autoloader; use App\Controllers\Home; +use Closure; use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\ReflectionHelper; use Config\Autoload; use Config\Modules; use Config\Services; use InvalidArgumentException; +use PHPUnit\Framework\MockObject\MockObject; use RuntimeException; use UnnamespacedClass; @@ -28,7 +31,10 @@ */ final class AutoloaderTest extends CIUnitTestCase { + use ReflectionHelper; + private Autoloader $loader; + private Closure $classLoader; protected function setUp(): void { @@ -50,13 +56,15 @@ protected function setUp(): void $this->loader = new Autoloader(); $this->loader->initialize($config, $modules)->register(); + + $this->classLoader = $this->getPrivateMethodInvoker($this->loader, 'loadInNamespace'); } protected function tearDown(): void { - $this->loader->unregister(); - parent::tearDown(); + + $this->loader->unregister(); } public function testLoadStoredClass(): void @@ -96,9 +104,10 @@ public function testInitializeTwice(): void public function testServiceAutoLoaderFromShareInstances(): void { - $autoloader = Services::autoloader(); + $classLoader = $this->getPrivateMethodInvoker(Services::autoloader(), 'loadInNamespace'); + // look for Home controller, as that should be in base repo - $actual = $autoloader->loadClass(Home::class); + $actual = $classLoader(Home::class); $expected = APPPATH . 'Controllers' . DIRECTORY_SEPARATOR . 'Home.php'; $this->assertSame($expected, realpath($actual) ?: $actual); } @@ -109,8 +118,10 @@ public function testServiceAutoLoader(): void $autoloader->initialize(new Autoload(), new Modules()); $autoloader->register(); + $classLoader = $this->getPrivateMethodInvoker($autoloader, 'loadInNamespace'); + // look for Home controller, as that should be in base repo - $actual = $autoloader->loadClass(Home::class); + $actual = $classLoader(Home::class); $expected = APPPATH . 'Controllers' . DIRECTORY_SEPARATOR . 'Home.php'; $this->assertSame($expected, realpath($actual) ?: $actual); @@ -119,41 +130,43 @@ public function testServiceAutoLoader(): void public function testExistingFile(): void { - $actual = $this->loader->loadClass(Home::class); + $actual = ($this->classLoader)(Home::class); $expected = APPPATH . 'Controllers' . DIRECTORY_SEPARATOR . 'Home.php'; $this->assertSame($expected, $actual); - $actual = $this->loader->loadClass('CodeIgniter\Helpers\array_helper'); + $actual = ($this->classLoader)('CodeIgniter\Helpers\array_helper'); $expected = SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR . 'array_helper.php'; $this->assertSame($expected, $actual); } public function testMatchesWithPrecedingSlash(): void { - $actual = $this->loader->loadClass(Home::class); + $actual = ($this->classLoader)(Home::class); $expected = APPPATH . 'Controllers' . DIRECTORY_SEPARATOR . 'Home.php'; $this->assertSame($expected, $actual); } public function testMatchesWithFileExtension(): void { - $actual = $this->loader->loadClass('\App\Controllers\Home.php'); - $expected = APPPATH . 'Controllers' . DIRECTORY_SEPARATOR . 'Home.php'; - $this->assertSame($expected, $actual); + /** @var Autoloader&MockObject $classLoader */ + $classLoader = $this->getMockBuilder(Autoloader::class)->onlyMethods(['loadInNamespace'])->getMock(); + $classLoader->expects($this->once())->method('loadInNamespace')->with(Home::class); + + $classLoader->loadClass('\App\Controllers\Home.php'); } public function testMissingFile(): void { - $this->assertFalse($this->loader->loadClass('\App\Missing\Classname')); + $this->assertFalse(($this->classLoader)('\App\Missing\Classname')); } public function testAddNamespaceWorks(): void { - $this->assertFalse($this->loader->loadClass('My\App\Class')); + $this->assertFalse(($this->classLoader)('My\App\Class')); $this->loader->addNamespace('My\App', __DIR__); - $actual = $this->loader->loadClass('My\App\AutoloaderTest'); + $actual = ($this->classLoader)('My\App\AutoloaderTest'); $expected = __FILE__; $this->assertSame($expected, $actual); @@ -168,11 +181,11 @@ public function testAddNamespaceMultiplePathsWorks(): void ], ]); - $actual = $this->loader->loadClass('My\App\App'); + $actual = ($this->classLoader)('My\App\App'); $expected = APPPATH . 'Config' . DIRECTORY_SEPARATOR . 'App.php'; $this->assertSame($expected, $actual); - $actual = $this->loader->loadClass('My\App\AutoloaderTest'); + $actual = ($this->classLoader)('My\App\AutoloaderTest'); $expected = __FILE__; $this->assertSame($expected, $actual); } @@ -183,7 +196,7 @@ public function testAddNamespaceStringToArray(): void $this->assertSame( __FILE__, - $this->loader->loadClass('App\Controllers\AutoloaderTest') + ($this->classLoader)('App\Controllers\AutoloaderTest') ); } @@ -201,15 +214,15 @@ public function testGetNamespaceGivesArray(): void public function testRemoveNamespace(): void { $this->loader->addNamespace('My\App', __DIR__); - $this->assertSame(__FILE__, $this->loader->loadClass('My\App\AutoloaderTest')); + $this->assertSame(__FILE__, ($this->classLoader)('My\App\AutoloaderTest')); $this->loader->removeNamespace('My\App'); - $this->assertFalse((bool) $this->loader->loadClass('My\App\AutoloaderTest')); + $this->assertFalse(($this->classLoader)('My\App\AutoloaderTest')); } public function testloadClassNonNamespaced(): void { - $this->assertFalse($this->loader->loadClass('Modules')); + $this->assertFalse(($this->classLoader)('Modules')); } public function testSanitizationContailsSpecialChars(): void diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 3cf33002d946..5cf06105d29f 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -109,6 +109,12 @@ Added Parameters - **Routing:** The third parameter ``Routing $routing`` has been added to ``RouteCollection::__construct()``. +Return Type Changes +------------------- + +- **Autoloader:** The return signatures of the `loadClass` and `loadClassmap` methods are made `void` + to be compatible as callbacks in `spl_autoload_register` and `spl_autoload_unregister` functions. + Enhancements ************ @@ -225,6 +231,8 @@ Changes ``Config\App::$forceGlobalSecureRequests = true`` sets the HTTP status code 307, which allows the HTTP request method to be preserved after the redirect. In previous versions, it was 302. +- The methods `Autoloader::loadClass()` and `Autoloader::loadClassmap()` are now both + marked `@internal`. Deprecations ************ From 00ef50fe0c0018e2969b2420d50b73b2a78dc114 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 6 Jul 2023 21:25:06 +0900 Subject: [PATCH 414/485] fix: remove unused parameters in Services::exceptions() and Exception --- system/Config/BaseService.php | 2 +- system/Config/Services.php | 11 ++--------- system/Debug/Exceptions.php | 7 +------ tests/system/Config/ServicesTest.php | 4 ++-- tests/system/Debug/ExceptionsTest.php | 7 +++---- 5 files changed, 9 insertions(+), 22 deletions(-) diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 133805a6a585..92995a81c0d3 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -101,7 +101,7 @@ * @method static CURLRequest curlrequest($options = [], ResponseInterface $response = null, App $config = null, $getShared = true) * @method static Email email($config = null, $getShared = true) * @method static EncrypterInterface encrypter(Encryption $config = null, $getShared = false) - * @method static Exceptions exceptions(ConfigExceptions $config = null, IncomingRequest $request = null, ResponseInterface $response = null, $getShared = true) + * @method static Exceptions exceptions(ConfigExceptions $config = null, $getShared = true) * @method static Filters filters(ConfigFilters $config = null, $getShared = true) * @method static Format format(ConfigFormat $config = null, $getShared = true) * @method static Honeypot honeypot(ConfigHoneyPot $config = null, $getShared = true) diff --git a/system/Config/Services.php b/system/Config/Services.php index 2004b6dcd2a8..ca2d700b3b14 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -257,25 +257,18 @@ public static function encrypter(?EncryptionConfig $config = null, $getShared = * - register_shutdown_function * * @return Exceptions - * - * @deprecated The parameter $request and $response are deprecated. */ public static function exceptions( ?ExceptionsConfig $config = null, - ?IncomingRequest $request = null, - ?ResponseInterface $response = null, bool $getShared = true ) { if ($getShared) { - return static::getSharedInstance('exceptions', $config, $request, $response); + return static::getSharedInstance('exceptions', $config); } $config ??= config(ExceptionsConfig::class); - // @TODO remove instantiation of Response in the future. - $response ??= AppServices::response(); - - return new Exceptions($config, $request, $response); + return new Exceptions($config); } /** diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 8f07681f3a31..3f22bc1b1c68 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -74,12 +74,7 @@ class Exceptions private ?Throwable $exceptionCaughtByExceptionHandler = null; - /** - * @param RequestInterface|null $request - * - * @deprecated The parameter $request and $response are deprecated. No longer used. - */ - public function __construct(ExceptionsConfig $config, $request, ResponseInterface $response) /** @phpstan-ignore-line */ + public function __construct(ExceptionsConfig $config) { // For backward compatibility $this->ob_level = ob_get_level(); diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index e1ef2ef28da2..b22995b5b218 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -127,13 +127,13 @@ public function testNewUnsharedEmailWithNonEmptyConfig(): void public function testNewExceptions(): void { - $actual = Services::exceptions(new Exceptions(), Services::request(), Services::response()); + $actual = Services::exceptions(new Exceptions()); $this->assertInstanceOf(\CodeIgniter\Debug\Exceptions::class, $actual); } public function testNewExceptionsWithNullConfig(): void { - $actual = Services::exceptions(null, null, null, false); + $actual = Services::exceptions(null, false); $this->assertInstanceOf(\CodeIgniter\Debug\Exceptions::class, $actual); } diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php index a77745ba8a78..4a30a32f1133 100644 --- a/tests/system/Debug/ExceptionsTest.php +++ b/tests/system/Debug/ExceptionsTest.php @@ -19,7 +19,6 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\ReflectionHelper; use Config\Exceptions as ExceptionsConfig; -use Config\Services; use ErrorException; use RuntimeException; @@ -52,7 +51,7 @@ protected function setUp(): void { parent::setUp(); - $this->exception = new Exceptions(new ExceptionsConfig(), Services::request(), Services::response()); + $this->exception = new Exceptions(new ExceptionsConfig()); } /** @@ -65,7 +64,7 @@ public function testDeprecationsOnPhp81DoNotThrow(): void $config->logDeprecations = true; $config->deprecationLogLevel = 'error'; - $this->exception = new Exceptions($config, Services::request(), Services::response()); + $this->exception = new Exceptions($config); $this->exception->initialize(); // this is only needed for IDEs not to complain that strlen does not accept explicit null @@ -89,7 +88,7 @@ public function testSuppressedDeprecationsAreLogged(): void $config->logDeprecations = true; $config->deprecationLogLevel = 'error'; - $this->exception = new Exceptions($config, Services::request(), Services::response()); + $this->exception = new Exceptions($config); $this->exception->initialize(); @trigger_error('Hello! I am a deprecation!', E_USER_DEPRECATED); From 62feafc00453579808be6041acb95587a526467a Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 2 Aug 2023 10:27:35 +0800 Subject: [PATCH 415/485] Remove trimming logic of `Autoloader::loadClass()` --- system/Autoloader/Autoloader.php | 3 --- tests/system/Autoloader/AutoloaderTest.php | 9 --------- 2 files changed, 12 deletions(-) diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index dc0b72cd6122..d5c155685e12 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -260,9 +260,6 @@ public function loadClassmap(string $class): void */ public function loadClass(string $class): void { - $class = trim($class, '\\'); - $class = str_ireplace('.php', '', $class); - $this->loadInNamespace($class); } diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index d877d66e406d..8097ce962b5a 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -146,15 +146,6 @@ public function testMatchesWithPrecedingSlash(): void $this->assertSame($expected, $actual); } - public function testMatchesWithFileExtension(): void - { - /** @var Autoloader&MockObject $classLoader */ - $classLoader = $this->getMockBuilder(Autoloader::class)->onlyMethods(['loadInNamespace'])->getMock(); - $classLoader->expects($this->once())->method('loadInNamespace')->with(Home::class); - - $classLoader->loadClass('\App\Controllers\Home.php'); - } - public function testMissingFile(): void { $this->assertFalse(($this->classLoader)('\App\Missing\Classname')); From 14b3b67e27f0b30fffca28774befca68b0675410 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Jul 2023 08:59:17 +0900 Subject: [PATCH 416/485] docs: add changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 5cf06105d29f..a9feb1198a9c 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -109,6 +109,14 @@ Added Parameters - **Routing:** The third parameter ``Routing $routing`` has been added to ``RouteCollection::__construct()``. +Removed Parameters +------------------ + +- **Services:** The second parameter ``$request`` and the third parameter + ``$response`` in ``Services::exceptions()`` have been removed. +- **Error Handling:** The second parameter ``$request`` and the third parameter + ``$response`` in ``CodeIgniter\Debug\Exceptions::__construct()`` have been removed. + Return Type Changes ------------------- From 1b09623e3f6c5a52af8db93bd2a12282572e8a97 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 7 Jul 2023 09:07:08 +0900 Subject: [PATCH 417/485] docs: add version to @deprecate --- system/Debug/Exceptions.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 3f22bc1b1c68..356c9d7fa0d1 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -37,7 +37,7 @@ class Exceptions * * @var int * - * @deprecated No longer used. Moved to BaseExceptionHandler. + * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ public $ob_level; @@ -47,7 +47,7 @@ class Exceptions * * @var string * - * @deprecated No longer used. Moved to BaseExceptionHandler. + * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ protected $viewPath; @@ -235,7 +235,7 @@ public function shutdownHandler() * * @return string The path and filename of the view file to use * - * @deprecated No longer used. Moved to ExceptionHandler. + * @deprecated 4.4.0 No longer used. Moved to ExceptionHandler. */ protected function determineView(Throwable $exception, string $templatePath): string { @@ -263,7 +263,7 @@ protected function determineView(Throwable $exception, string $templatePath): st /** * Given an exception and status code will display the error to the client. * - * @deprecated No longer used. Moved to BaseExceptionHandler. + * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ protected function render(Throwable $exception, int $statusCode) { @@ -305,7 +305,7 @@ protected function render(Throwable $exception, int $statusCode) /** * Gathers the variables that will be made available to the view. * - * @deprecated No longer used. Moved to BaseExceptionHandler. + * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ protected function collectVars(Throwable $exception, int $statusCode): array { @@ -466,7 +466,7 @@ public static function cleanPath(string $file): string * Describes memory usage in real-world units. Intended for use * with memory_get_usage, etc. * - * @deprecated No longer used. Moved to BaseExceptionHandler. + * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ public static function describeMemory(int $bytes): string { @@ -486,7 +486,7 @@ public static function describeMemory(int $bytes): string * * @return bool|string * - * @deprecated No longer used. Moved to BaseExceptionHandler. + * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ public static function highlightFile(string $file, int $lineNumber, int $lines = 15) { From c7135801417c924917e645740ba4a12a4ca6f2f8 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 2 Aug 2023 10:30:50 +0800 Subject: [PATCH 418/485] Add Closure signature --- tests/system/Autoloader/AutoloaderTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index 8097ce962b5a..4c81537dbf09 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -20,7 +20,6 @@ use Config\Modules; use Config\Services; use InvalidArgumentException; -use PHPUnit\Framework\MockObject\MockObject; use RuntimeException; use UnnamespacedClass; @@ -34,6 +33,10 @@ final class AutoloaderTest extends CIUnitTestCase use ReflectionHelper; private Autoloader $loader; + + /** + * @phpstan-var Closure(string): (false|string) + */ private Closure $classLoader; protected function setUp(): void From b93b35b908e7985ec5226109c4605a4e68dcc46c Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 2 Aug 2023 10:37:25 +0800 Subject: [PATCH 419/485] Add to changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 5cf06105d29f..4747e953fc29 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -58,6 +58,12 @@ Property Name The property ``Factories::$basenames`` has been renamed to ``$aliases``. +Autoloader +---------- + +Previously, CodeIgniter's autoloader allowed loading class names ending with the `.php` extension. This means instantiating objects like `new Foo.php()` was possible +and would instantiate as `new Foo()`. Since `Foo.php` is an invalid class name, this behavior of the autoloader is changed. Now, instantiating such classes would fail. + .. _v440-interface-changes: Interface Changes From f464fac1549352288e0439304bab9739481b098e Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 4 Aug 2023 10:24:38 +0900 Subject: [PATCH 420/485] test: fix data provider method names --- tests/system/HTTP/SiteURITest.php | 4 ++-- tests/system/Helpers/ArrayHelperTest.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 48424bc6fe79..8b5e10139d24 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -65,7 +65,7 @@ public function provideConstructor(): iterable return array_merge($this->provideURIs(), $this->provideRelativePathWithQueryOrFragment()); } - public function provideURIs(): iterable + public function provideSetPath(): iterable { return [ '' => [ @@ -315,7 +315,7 @@ public function testConstructorInvalidBaseURL() } /** - * @dataProvider provideURIs + * @dataProvider provideSetPath */ public function testSetPath( string $baseURL, diff --git a/tests/system/Helpers/ArrayHelperTest.php b/tests/system/Helpers/ArrayHelperTest.php index 8e5584703c91..89afae1dae31 100644 --- a/tests/system/Helpers/ArrayHelperTest.php +++ b/tests/system/Helpers/ArrayHelperTest.php @@ -500,7 +500,7 @@ public function provideArrayFlattening(): iterable } /** - * @dataProvider arrayGroupByIncludeEmptyProvider + * @dataProvider provideArrayGroupByIncludeEmpty */ public function testArrayGroupByIncludeEmpty(array $indexes, array $data, array $expected): void { @@ -510,7 +510,7 @@ public function testArrayGroupByIncludeEmpty(array $indexes, array $data, array } /** - * @dataProvider arrayGroupByExcludeEmptyProvider + * @dataProvider provideArrayGroupByExcludeEmpty */ public function testArrayGroupByExcludeEmpty(array $indexes, array $data, array $expected): void { @@ -519,7 +519,7 @@ public function testArrayGroupByExcludeEmpty(array $indexes, array $data, array $this->assertSame($expected, $actual, 'array excluding empty not the same'); } - public function arrayGroupByIncludeEmptyProvider(): iterable + public function provideArrayGroupByIncludeEmpty(): iterable { yield 'simple group-by test' => [ ['color'], @@ -930,7 +930,7 @@ public function arrayGroupByIncludeEmptyProvider(): iterable ]; } - public function arrayGroupByExcludeEmptyProvider(): iterable + public function provideArrayGroupByExcludeEmpty(): iterable { yield 'simple group-by test' => [ ['color'], From cfbc04d7fbc1003bca2dc1d70dc764f29d14f4dd Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 4 Aug 2023 11:02:39 +0900 Subject: [PATCH 421/485] test: update method name --- tests/system/HTTP/SiteURITest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 8b5e10139d24..d0f84673988f 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -62,7 +62,7 @@ public function testConstructor( public function provideConstructor(): iterable { - return array_merge($this->provideURIs(), $this->provideRelativePathWithQueryOrFragment()); + return array_merge($this->provideSetPath(), $this->provideRelativePathWithQueryOrFragment()); } public function provideSetPath(): iterable From b7849fe4d7afb4dd67ecfe05b6fdfe0b1251ef6a Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 5 Aug 2023 08:22:33 +0900 Subject: [PATCH 422/485] chore: update phpstan-baseline.php --- phpstan-baseline.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 636c4db7b92c..6390a71ee03a 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -11,16 +11,6 @@ 'count' => 1, 'path' => __DIR__ . '/app/Config/View.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$callback of function spl_autoload_register expects \\(callable\\(string\\)\\: void\\)\\|null, array\\{\\$this\\(CodeIgniter\\\\Autoloader\\\\Autoloader\\), \'loadClass\'\\} given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Autoloader/Autoloader.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$callback of function spl_autoload_register expects \\(callable\\(string\\)\\: void\\)\\|null, array\\{\\$this\\(CodeIgniter\\\\Autoloader\\\\Autoloader\\), \'loadClassmap\'\\} given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Autoloader/Autoloader.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:chunk\\(\\) has parameter \\$userFunc with no signature specified for Closure\\.$#', 'count' => 1, From fb1bf0030d0a558967307a5f02cc473b4a9bcb9d Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 5 Aug 2023 08:23:00 +0900 Subject: [PATCH 423/485] test: update coding style --- tests/system/HTTP/SiteURITest.php | 2 +- tests/system/Helpers/ArrayHelperTest.php | 4 ++-- tests/system/View/TableTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index d0f84673988f..392394086d30 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -65,7 +65,7 @@ public function provideConstructor(): iterable return array_merge($this->provideSetPath(), $this->provideRelativePathWithQueryOrFragment()); } - public function provideSetPath(): iterable + public static function provideSetPath(): iterable { return [ '' => [ diff --git a/tests/system/Helpers/ArrayHelperTest.php b/tests/system/Helpers/ArrayHelperTest.php index 3891d3e6a317..9a91c807c4b4 100644 --- a/tests/system/Helpers/ArrayHelperTest.php +++ b/tests/system/Helpers/ArrayHelperTest.php @@ -519,7 +519,7 @@ public function testArrayGroupByExcludeEmpty(array $indexes, array $data, array $this->assertSame($expected, $actual, 'array excluding empty not the same'); } - public function provideArrayGroupByIncludeEmpty(): iterable + public static function provideArrayGroupByIncludeEmpty(): iterable { yield 'simple group-by test' => [ ['color'], @@ -930,7 +930,7 @@ public function provideArrayGroupByIncludeEmpty(): iterable ]; } - public function provideArrayGroupByExcludeEmpty(): iterable + public static function provideArrayGroupByExcludeEmpty(): iterable { yield 'simple group-by test' => [ ['color'], diff --git a/tests/system/View/TableTest.php b/tests/system/View/TableTest.php index 0a6fea78817e..3c607930616a 100644 --- a/tests/system/View/TableTest.php +++ b/tests/system/View/TableTest.php @@ -791,7 +791,7 @@ public function testGenerateOrderedColumns(array $heading, array $row, string $e $this->assertStringContainsString($expectContainsString, $generated); } - public function orderedColumnUsecases(): iterable + public static function orderedColumnUsecases(): iterable { yield from [ 'reorder example #1' => [ From 910c2039a696fd911d16efe07f3841dd2551d21c Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 9 Aug 2023 17:20:05 +0900 Subject: [PATCH 424/485] docs: group Autoloader items --- user_guide_src/source/changelogs/v4.4.0.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 32f80d74ff1b..df89ce2b56c2 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -233,20 +233,21 @@ Changes - Routing settings have been moved to **app/Config/Routing.php** config file. See :ref:`Upgrading Guide `. - **DownloadResponse:** When generating response headers, does not replace the ``Content-Disposition`` header if it was previously specified. -- **Autoloader:** Before v4.4.0, CodeIgniter autoloader did not allow special - characters that are illegal in filenames on certain operating systems. - The symbols that can be used are ``/``, ``_``, ``.``, ``:``, ``\`` and space. - So if you installed CodeIgniter under the folder that contains the special - characters like ``(``, ``)``, etc., CodeIgniter didn't work. Since v4.4.0, - this restriction has been removed. +- **Autoloader:** + - Before v4.4.0, CodeIgniter autoloader did not allow special + characters that are illegal in filenames on certain operating systems. + The symbols that can be used are ``/``, ``_``, ``.``, ``:``, ``\`` and space. + So if you installed CodeIgniter under the folder that contains the special + characters like ``(``, ``)``, etc., CodeIgniter didn't work. Since v4.4.0, + this restriction has been removed. + - The methods `Autoloader::loadClass()` and `Autoloader::loadClassmap()` are now both + marked `@internal`. - **RouteCollection:** The array structure of the protected property ``$routes`` has been modified for performance. - **HSTS:** Now :php:func:`force_https()` or ``Config\App::$forceGlobalSecureRequests = true`` sets the HTTP status code 307, which allows the HTTP request method to be preserved after the redirect. In previous versions, it was 302. -- The methods `Autoloader::loadClass()` and `Autoloader::loadClassmap()` are now both - marked `@internal`. Deprecations ************ From cb3e76849b86598b4486c6e0f868961d7aa5d8bd Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 9 Aug 2023 17:21:38 +0900 Subject: [PATCH 425/485] docs: fix text decoration --- user_guide_src/source/changelogs/v4.4.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index df89ce2b56c2..6d27223fc02a 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -240,8 +240,8 @@ Changes So if you installed CodeIgniter under the folder that contains the special characters like ``(``, ``)``, etc., CodeIgniter didn't work. Since v4.4.0, this restriction has been removed. - - The methods `Autoloader::loadClass()` and `Autoloader::loadClassmap()` are now both - marked `@internal`. + - The methods ``Autoloader::loadClass()`` and ``Autoloader::loadClassmap()`` are now both + marked ``@internal``. - **RouteCollection:** The array structure of the protected property ``$routes`` has been modified for performance. - **HSTS:** Now :php:func:`force_https()` or From 7c87f07f4c9f2bb77ef74dd34675a774c24f3609 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Jul 2023 18:02:03 +0900 Subject: [PATCH 426/485] refactor: move callExit() to index.php --- public/index.php | 4 ++++ system/CodeIgniter.php | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/public/index.php b/public/index.php index a6d943a9d861..d031ea10944a 100644 --- a/public/index.php +++ b/public/index.php @@ -67,3 +67,7 @@ */ $app->run(); + +// Exits the application, setting the exit code for CLI-based applications +// that might be watching. +exit(EXIT_SUCCESS); diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 760cd7324403..8bdcc15bc16e 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -372,7 +372,6 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon } $this->sendResponse(); - $this->callExit(EXIT_SUCCESS); } /** @@ -1088,6 +1087,8 @@ protected function sendResponse() * without actually stopping script execution. * * @param int $code + * + * @deprecated 4.4.0 No longer Used. Moved to index.php. */ protected function callExit($code) { From 5b26b28350129b17343c0209ca1c14433aa9f3b4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 9 Aug 2023 17:45:37 +0900 Subject: [PATCH 427/485] docs: add changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 6d27223fc02a..483637095007 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -263,6 +263,7 @@ Deprecations - ``CodeIgniter::cache()`` method is deprecated. No longer used. Use ``ResponseCache`` instead. - ``CodeIgniter::cachePage()`` method is deprecated. No longer used. Use ``ResponseCache`` instead. - ``CodeIgniter::generateCacheName()`` method is deprecated. No longer used. Use ``ResponseCache`` instead. + - ``CodeIgniter::callExit()`` method is deprecated. No longer used. - **RedirectException:** ``\CodeIgniter\Router\Exceptions\RedirectException`` is deprecated. Use ``\CodeIgniter\HTTP\Exceptions\RedirectException`` instead. - **Session:** The property ``$sessionDriverName``, ``$sessionCookieName``, ``$sessionExpiration``, ``$sessionSavePath``, ``$sessionMatchIP``, From 006d38ce6f283ca68845d7ff114cf1d7659108ba Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 11 Aug 2023 12:07:48 +0900 Subject: [PATCH 428/485] docs: add @deprecated in MockCodeIgniter --- system/Test/Mock/MockCodeIgniter.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/Test/Mock/MockCodeIgniter.php b/system/Test/Mock/MockCodeIgniter.php index e6d6ac0f6c61..200c92fa0396 100644 --- a/system/Test/Mock/MockCodeIgniter.php +++ b/system/Test/Mock/MockCodeIgniter.php @@ -17,6 +17,11 @@ class MockCodeIgniter extends CodeIgniter { protected ?string $context = 'web'; + /** + * @param int $code + * + * @deprecated 4.4.0 No longer Used. Moved to index.php. + */ protected function callExit($code) { // Do not call exit() in testing. From 13251a2e3a19c1ae24bf00f0ed3c74490bd4178f Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 13 Aug 2023 12:05:40 +0900 Subject: [PATCH 429/485] docs: add changelog and ugrade note --- user_guide_src/source/changelogs/v4.4.0.rst | 8 ++++++++ .../source/installation/upgrade_440.rst | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 483637095007..4b7b065cd6e2 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -64,6 +64,14 @@ Autoloader Previously, CodeIgniter's autoloader allowed loading class names ending with the `.php` extension. This means instantiating objects like `new Foo.php()` was possible and would instantiate as `new Foo()`. Since `Foo.php` is an invalid class name, this behavior of the autoloader is changed. Now, instantiating such classes would fail. +.. _v440-codeigniter-and-exit: + +CodeIgniter and exit() +---------------------- + +The ``CodeIgniter::run()`` method no longer calls ``exit(EXIT_SUCCESS)``. The +exit call is moved to **public/index.php**. + .. _v440-interface-changes: Interface Changes diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index b740c68d7b5f..ffb8751c5206 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -117,6 +117,24 @@ match the new array structure. Mandatory File Changes ********************** +index.php +========= + +The following file received significant changes and +**you must merge the updated versions** with your application: + +- ``public/index.php`` (see also :ref:`v440-codeigniter-and-exit`) + +.. important:: If you don't update the above file, CodeIgniter will not work + properly after running ``composer update``. + + The upgrade procedure, for example, is as follows: + + .. code-block:: console + + composer update + cp vendor/codeigniter4/framework/public/index.php public/index.php + Config Files ============ From 83c611c036cc6ef6c3b185092e2c9aac53a55a29 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 16 Aug 2023 10:40:30 +0900 Subject: [PATCH 430/485] style: composer cs-fix --- system/Router/AutoRouterImproved.php | 1 + system/Router/RouteCollection.php | 1 + tests/system/ControllerTest.php | 1 + 3 files changed, 3 insertions(+) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 6e0583d2a1e9..b6b81c70f2ef 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -125,6 +125,7 @@ private function createSegments(string $uri): array { $segments = explode('/', $uri); $segments = array_filter($segments, static fn ($segment) => $segment !== ''); + // numerically reindex the array, removing gaps return array_values($segments); } diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 961369342bc5..82c6a6dd5bbe 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1455,6 +1455,7 @@ protected function create(string $verb, string $from, $to, ?array $options = nul } $routeKey = $from; + // Replace our regex pattern placeholders with the actual thing // so that the Router doesn't need to know about any of this. foreach ($this->placeholders as $tag => $pattern) { diff --git a/tests/system/ControllerTest.php b/tests/system/ControllerTest.php index 73879dca9a2d..668cf9b1bc68 100644 --- a/tests/system/ControllerTest.php +++ b/tests/system/ControllerTest.php @@ -75,6 +75,7 @@ public function testConstructorHTTPS(): void { $original = $_SERVER; $_SERVER = ['HTTPS' => 'on']; + // make sure we can instantiate one try { $this->controller = new class () extends Controller { From d01c783a6f99e851882902dba35dbfc9f8d013d2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 20 Feb 2023 15:20:20 +0900 Subject: [PATCH 431/485] refactor: current URI creation --- system/Config/Services.php | 11 ++- system/HTTP/IncomingRequest.php | 59 +++-------- system/HTTP/URI.php | 5 - system/Test/FeatureTestTrait.php | 21 ++-- tests/_support/Config/Services.php | 10 ++ tests/system/HTTP/IncomingRequestTest.php | 97 ++++++------------- tests/system/HTTP/NegotiateTest.php | 3 +- tests/system/HTTP/RedirectResponseTest.php | 7 +- .../SiteURIFactoryDetectRoutePathTest.php | 81 ++++++++++++++++ tests/system/HTTP/URITest.php | 37 ++++--- 10 files changed, 186 insertions(+), 145 deletions(-) diff --git a/system/Config/Services.php b/system/Config/Services.php index ffca4fa804ff..2e1e7879c087 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -38,6 +38,7 @@ use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\HTTP\SiteURIFactory; use CodeIgniter\HTTP\URI; use CodeIgniter\HTTP\UserAgent; use CodeIgniter\Images\Handlers\BaseHandler; @@ -52,6 +53,7 @@ use CodeIgniter\Session\Handlers\Database\PostgreHandler; use CodeIgniter\Session\Handlers\DatabaseHandler; use CodeIgniter\Session\Session; +use CodeIgniter\Superglobals; use CodeIgniter\Throttle\Throttler; use CodeIgniter\Typography\Typography; use CodeIgniter\Validation\Validation; @@ -744,7 +746,7 @@ public static function toolbar(?ToolbarConfig $config = null, bool $getShared = * * @param string $uri * - * @return URI + * @return URI The current URI if $uri is null. */ public static function uri(?string $uri = null, bool $getShared = true) { @@ -752,6 +754,13 @@ public static function uri(?string $uri = null, bool $getShared = true) return static::getSharedInstance('uri', $uri); } + if ($uri === null) { + $appConfig = config(App::class); + $factory = new SiteURIFactory($appConfig, new Superglobals()); + + return $factory->createFromGlobals(); + } + return new URI($uri); } diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 7de4c37b4f15..4f0f1cb0d85d 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -175,7 +175,12 @@ public function __construct($config, ?URI $uri = null, $body = 'php://input', ?U parent::__construct($config); - $this->detectURI($config->uriProtocol, $config->baseURL); + if ($uri instanceof SiteURI) { + $this->setPath($uri->getRoutePath()); + } else { + $this->setPath($uri->getPath()); + } + $this->detectLocale($config); } @@ -227,9 +232,9 @@ public function detectLocale($config) * either provided by the user in the baseURL Config setting, or * determined from the environment as needed. * - * @deprecated $protocol and $baseURL are deprecated. No longer used. - * * @return void + * + * @deprecated No longer used. */ protected function detectURI(string $protocol, string $baseURL) { @@ -447,7 +452,7 @@ public function isSecure(): bool } /** - * Sets the relative path and updates the URI object. + * Sets the URI path relative to baseURL. * * Note: Since current_url() accesses the shared request * instance, this can be used to change the "current URL" @@ -457,51 +462,13 @@ public function isSecure(): bool * @param App|null $config Optional alternate config to use * * @return $this + * + * @deprecated This method will be private. The parameter $config is deprecated. No longer used. */ public function setPath(string $path, ?App $config = null) { $this->path = $path; - // @TODO remove this. The path of the URI object should be a full URI path, - // not a URI path relative to baseURL. - $this->uri->setPath($path); - - $config ??= $this->config; - - // It's possible the user forgot a trailing slash on their - // baseURL, so let's help them out. - $baseURL = ($config->baseURL === '') ? $config->baseURL : rtrim($config->baseURL, '/ ') . '/'; - - // Based on our baseURL and allowedHostnames provided by the developer - // and HTTP_HOST, set our current domain name, scheme. - if ($baseURL !== '') { - $host = $this->determineHost($config, $baseURL); - - // Set URI::$baseURL - $uri = new URI($baseURL); - $currentBaseURL = (string) $uri->setHost($host); - $this->uri->setBaseURL($currentBaseURL); - - $this->uri->setScheme(parse_url($baseURL, PHP_URL_SCHEME)); - $this->uri->setHost($host); - $this->uri->setPort(parse_url($baseURL, PHP_URL_PORT)); - - // Ensure we have any query vars - $this->uri->setQuery($_SERVER['QUERY_STRING'] ?? ''); - - // Check if the scheme needs to be coerced into its secure version - if ($config->forceGlobalSecureRequests && $this->uri->getScheme() === 'http') { - $this->uri->setScheme('https'); - } - } elseif (! is_cli()) { - // Do not change exit() to exception; Request is initialized before - // setting the exception handler, so if an exception is raised, an - // error will be displayed even if in the production environment. - // @codeCoverageIgnoreStart - exit('You have an empty or invalid baseURL. The baseURL value must be set in app/Config/App.php, or through the .env file.'); - // @codeCoverageIgnoreEnd - } - return $this; } @@ -535,10 +502,6 @@ private function determineHost(App $config, string $baseURL): string */ public function getPath(): string { - if ($this->path === null) { - $this->detectPath($this->config->uriProtocol); - } - return $this->path; } diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 3eef2371943a..429aa73a7b7f 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -94,11 +94,6 @@ class URI /** * URI path. * - * Note: The constructor of the IncomingRequest class changes the path of - * the URI object held by the IncomingRequest class to a path relative - * to the baseURL. If the baseURL contains subfolders, this value - * will be different from the current URI path. - * * @var string */ protected $path; diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index bde29f1b03f5..ded49bd35011 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -15,6 +15,7 @@ use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Request; +use CodeIgniter\HTTP\SiteURI; use CodeIgniter\HTTP\URI; use Config\App; use Config\Services; @@ -265,15 +266,23 @@ public function options(string $path, ?array $params = null) */ protected function setupRequest(string $method, ?string $path = null): IncomingRequest { - $path = URI::removeDotSegments($path); - $config = config(App::class); - $request = Services::request($config, false); + $config = config(App::class); + $uri = new SiteURI($config); // $path may have a query in it - $parts = explode('?', $path); - $_SERVER['QUERY_STRING'] = $parts[1] ?? ''; + $path = URI::removeDotSegments($path); + $parts = explode('?', $path); + $path = $parts[0]; + $query = $parts[1] ?? ''; + + $_SERVER['QUERY_STRING'] = $query; + + $uri->setPath($path); + + Services::injectMock('uri', $uri); + + $request = Services::request($config, false); - $request->setPath($parts[0]); $request->setMethod($method); $request->setProtocolVersion('1.1'); diff --git a/tests/_support/Config/Services.php b/tests/_support/Config/Services.php index 4a942a053433..9a553d0116a0 100644 --- a/tests/_support/Config/Services.php +++ b/tests/_support/Config/Services.php @@ -11,7 +11,10 @@ namespace Tests\Support\Config; +use CodeIgniter\HTTP\SiteURIFactory; use CodeIgniter\HTTP\URI; +use CodeIgniter\Superglobals; +use Config\App; use Config\Services as BaseServices; use RuntimeException; @@ -41,6 +44,13 @@ public static function uri(?string $uri = null, bool $getShared = true) return static::getSharedInstance('uri', $uri); } + if ($uri === null) { + $appConfig = config(App::class); + $factory = new SiteURIFactory($appConfig, new Superglobals()); + + return $factory->createFromGlobals(); + } + return new URI($uri); } } diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 0065ab430fc7..3855a4b85c94 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -35,11 +35,22 @@ protected function setUp(): void { parent::setUp(); - $this->request = new IncomingRequest(new App(), new URI(), null, new UserAgent()); + $config = new App(); + $this->request = $this->createRequest($config); $_POST = $_GET = $_SERVER = $_REQUEST = $_ENV = $_COOKIE = $_SESSION = []; } + private function createRequest(?App $config = null, $body = null, ?string $path = null): IncomingRequest + { + $config ??= new App(); + $path ??= ''; + + $uri = new SiteURI($config, $path); + + return new IncomingRequest($config, $uri, $body, new UserAgent()); + } + public function testCanGrabRequestVars(): void { $_REQUEST['TEST'] = 5; @@ -186,7 +197,7 @@ public function testSetLocaleSaves(): void $config->defaultLocale = 'es'; $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), null, new UserAgent()); + $request = $this->createRequest($config); $request->setLocale('en'); $this->assertSame('en', $request->getLocale()); @@ -199,7 +210,7 @@ public function testSetBadLocale(): void $config->defaultLocale = 'es'; $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), null, new UserAgent()); + $request = $this->createRequest($config); $request->setLocale('xx'); $this->assertSame('es', $request->getLocale()); @@ -232,7 +243,7 @@ public function testNegotiatesLocale(): void $config->supportedLocales = ['fr', 'en']; $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), null, new UserAgent()); + $request = $this->createRequest($config); $this->assertSame($config->defaultLocale, $request->getDefaultLocale()); $this->assertSame('fr', $request->getLocale()); @@ -247,7 +258,7 @@ public function testNegotiatesLocaleOnlyBroad(): void $config->supportedLocales = ['fr', 'en']; $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), null, new UserAgent()); + $request = $this->createRequest($config); $this->assertSame($config->defaultLocale, $request->getDefaultLocale()); $this->assertSame('fr', $request->getLocale()); @@ -306,7 +317,7 @@ public function testCanGrabGetRawJSON(): void $config = new App(); $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); + $request = $this->createRequest($config, $json); $this->assertSame($expected, $request->getJSON(true)); } @@ -327,7 +338,7 @@ public function testCanGetAVariableFromJson(): void $config = new App(); $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); + $request = $this->createRequest($config, $json); $this->assertSame('bar', $request->getJsonVar('foo')); $this->assertNull($request->getJsonVar('notExists')); @@ -361,7 +372,7 @@ public function testGetJsonVarAsArray(): void $config = new App(); $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); + $request = $this->createRequest($config, $json); $jsonVar = $request->getJsonVar('baz', true); $this->assertIsArray($jsonVar); @@ -381,7 +392,7 @@ public function testGetJsonVarCanFilter(): void $config = new App(); $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); + $request = $this->createRequest($config, $json); $this->assertFalse($request->getJsonVar('foo', false, FILTER_VALIDATE_INT)); } @@ -402,7 +413,7 @@ public function testGetJsonVarCanFilterArray(): void $config = new App(); $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); + $request = $this->createRequest($config, $json); $request->setHeader('Content-Type', 'application/json'); $expected = [ @@ -446,7 +457,7 @@ public function testGetVarWorksWithJson(): void $config = new App(); $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); + $request = $this->createRequest($config, $json); $request->setHeader('Content-Type', 'application/json'); $this->assertSame('bar', $request->getVar('foo')); @@ -473,12 +484,13 @@ public function testGetVarWorksWithJsonAndGetParams(): void $_REQUEST['foo'] = 'bar'; $_REQUEST['fizz'] = 'buzz'; - $request = new IncomingRequest($config, new URI('http://example.com/path?foo=bar&fizz=buzz'), 'php://input', new UserAgent()); + $request = $this->createRequest($config, null); $request = $request->withMethod('GET'); // JSON type $request->setHeader('Content-Type', 'application/json'); + // The body is null, so this works. $this->assertSame('bar', $request->getVar('foo')); $this->assertSame('buzz', $request->getVar('fizz')); @@ -501,7 +513,7 @@ public function testGetJsonVarReturnsNullFromNullBody(): void $config = new App(); $config->baseURL = 'http://example.com/'; $json = null; - $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); + $request = $this->createRequest($config, $json); $this->assertNull($request->getJsonVar('myKey')); } @@ -511,7 +523,7 @@ public function testgetJSONReturnsNullFromNullBody(): void $config = new App(); $config->baseURL = 'http://example.com/'; $json = null; - $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); + $request = $this->createRequest($config, $json); $this->assertNull($request->getJSON()); } @@ -529,7 +541,7 @@ public function testCanGrabGetRawInput(): void $config = new App(); $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), $rawstring, new UserAgent()); + $request = $this->createRequest($config, $rawstring); $this->assertSame($expected, $request->getRawInput()); } @@ -627,7 +639,7 @@ public function testCanGrabGetRawInputVar($rawstring, $var, $expected, $filter, $config = new App(); $config->baseURL = 'http://example.com/'; - $request = new IncomingRequest($config, new URI(), $rawstring, new UserAgent()); + $request = $this->createRequest($config, $rawstring); $this->assertSame($expected, $request->getRawInputVar($var, $filter, $flag)); } @@ -723,7 +735,7 @@ public function testUserAgent(): void $_SERVER['HTTP_USER_AGENT'] = 'Mozilla'; $config = new App(); - $request = new IncomingRequest($config, new URI(), null, new UserAgent()); + $request = $this->createRequest($config); $this->assertSame('Mozilla', $request->getUserAgent()->__toString()); } @@ -838,7 +850,7 @@ public function testGetPostSecondStreams(): void public function testWithFalseBody(): void { // Use `false` here to simulate file_get_contents returning a false value - $request = new IncomingRequest(new App(), new URI(), false, new UserAgent()); + $request = $this->createRequest(null, false); $this->assertNotFalse($request->getBody()); $this->assertNull($request->getBody()); @@ -888,49 +900,11 @@ public function testExtensionPHP($path, $detectPath): void public function testGetPath(): void { - $_SERVER['REQUEST_URI'] = '/index.php/fruits/banana'; - $_SERVER['SCRIPT_NAME'] = '/index.php'; - - $request = new IncomingRequest(new App(), new URI(), null, new UserAgent()); - - $this->assertSame('fruits/banana', $request->getPath()); - } - - public function testGetPathIsRelative(): void - { - $_SERVER['REQUEST_URI'] = '/sub/folder/index.php/fruits/banana'; - $_SERVER['SCRIPT_NAME'] = '/sub/folder/index.php'; - - $request = new IncomingRequest(new App(), new URI(), null, new UserAgent()); - - $this->assertSame('fruits/banana', $request->getPath()); - } - - public function testGetPathStoresDetectedValue(): void - { - $_SERVER['REQUEST_URI'] = '/fruits/banana'; - $_SERVER['SCRIPT_NAME'] = '/index.php'; - - $request = new IncomingRequest(new App(), new URI(), null, new UserAgent()); - - $_SERVER['REQUEST_URI'] = '/candy/snickers'; + $request = $this->createRequest(null, null, 'fruits/banana'); $this->assertSame('fruits/banana', $request->getPath()); } - public function testGetPathIsRediscovered(): void - { - $_SERVER['REQUEST_URI'] = '/fruits/banana'; - $_SERVER['SCRIPT_NAME'] = '/index.php'; - - $request = new IncomingRequest(new App(), new URI(), null, new UserAgent()); - - $_SERVER['REQUEST_URI'] = '/candy/snickers'; - $request->detectPath(); - - $this->assertSame('candy/snickers', $request->getPath()); - } - public function testSetPath(): void { $request = new IncomingRequest(new App(), new URI(), null, new UserAgent()); @@ -940,15 +914,6 @@ public function testSetPath(): void $this->assertSame('foobar', $request->getPath()); } - public function testSetPathUpdatesURI(): void - { - $request = new IncomingRequest(new App(), new URI(), null, new UserAgent()); - - $request->setPath('apples'); - - $this->assertSame('apples', $request->getUri()->getPath()); - } - public function testGetIPAddressNormal(): void { $expected = '123.123.123.123'; diff --git a/tests/system/HTTP/NegotiateTest.php b/tests/system/HTTP/NegotiateTest.php index 4323e80c2b42..0784bd562b54 100644 --- a/tests/system/HTTP/NegotiateTest.php +++ b/tests/system/HTTP/NegotiateTest.php @@ -29,7 +29,8 @@ protected function setUp(): void { parent::setUp(); - $this->request = new IncomingRequest(new App(), new URI(), null, new UserAgent()); + $config = new App(); + $this->request = new IncomingRequest($config, new SiteURI($config), null, new UserAgent()); $this->negotiate = new Negotiate($this->request); } diff --git a/tests/system/HTTP/RedirectResponseTest.php b/tests/system/HTTP/RedirectResponseTest.php index b6d74c442421..a15117e9324a 100644 --- a/tests/system/HTTP/RedirectResponseTest.php +++ b/tests/system/HTTP/RedirectResponseTest.php @@ -53,7 +53,7 @@ protected function setUp(): void $this->request = new MockIncomingRequest( $this->config, - new URI('http://example.com'), + new SiteURI($this->config), null, new UserAgent() ); @@ -186,7 +186,8 @@ public function testWith(): void public function testRedirectBack(): void { $_SERVER['HTTP_REFERER'] = 'http://somewhere.com'; - $this->request = new MockIncomingRequest($this->config, new URI('http://somewhere.com'), null, new UserAgent()); + + $this->request = new MockIncomingRequest($this->config, new SiteURI($this->config), null, new UserAgent()); Services::injectMock('request', $this->request); $response = new RedirectResponse(new App()); @@ -222,7 +223,7 @@ public function testRedirectRouteBaseUrl(): void $config->baseURL = 'http://example.com/test/'; Factories::injectMock('config', 'App', $config); - $request = new MockIncomingRequest($config, new URI('http://example.com/test/'), null, new UserAgent()); + $request = new MockIncomingRequest($config, new SiteURI($config), null, new UserAgent()); Services::injectMock('request', $request); $response = new RedirectResponse(new App()); diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index b207d1134dc4..1e96bf065b92 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -157,6 +157,54 @@ public function testRequestURISuppressed() $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); } + public function testRequestURIGetPath() + { + // /index.php/fruits/banana + $_SERVER['REQUEST_URI'] = '/index.php/fruits/banana'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $this->assertSame('fruits/banana', $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURIPathIsRelative() + { + // /sub/folder/index.php/fruits/banana + $_SERVER['REQUEST_URI'] = '/sub/folder/index.php/fruits/banana'; + $_SERVER['SCRIPT_NAME'] = '/sub/folder/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $this->assertSame('fruits/banana', $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURIStoresDetectedPath() + { + // /fruits/banana + $_SERVER['REQUEST_URI'] = '/fruits/banana'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $_SERVER['REQUEST_URI'] = '/candy/snickers'; + + $this->assertSame('fruits/banana', $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURIPathIsNeverRediscovered() + { + $_SERVER['REQUEST_URI'] = '/fruits/banana'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createSiteURIFactory($_SERVER); + + $_SERVER['REQUEST_URI'] = '/candy/snickers'; + $factory->detectRoutePath('REQUEST_URI'); + + $this->assertSame('fruits/banana', $factory->detectRoutePath('REQUEST_URI')); + } + public function testQueryString() { // /index.php?/ci/woot @@ -228,4 +276,37 @@ public function testPathInfoSubfolder() $expected = 'woot'; $this->assertSame($expected, $factory->detectRoutePath('PATH_INFO')); } + + /** + * @dataProvider providePathChecks + * + * @param string $path + * @param string $detectPath + */ + public function testExtensionPHP($path, $detectPath) + { + $config = new App(); + $config->baseURL = 'http://example.com/'; + + $_SERVER['REQUEST_URI'] = $path; + $_SERVER['SCRIPT_NAME'] = $path; + + $factory = $this->createSiteURIFactory($_SERVER, $config); + + $this->assertSame($detectPath, $factory->detectRoutePath()); + } + + public function providePathChecks(): iterable + { + return [ + 'not /index.php' => [ + '/test.php', + '/', + ], + '/index.php' => [ + '/index.php', + '/', + ], + ]; + } } diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index 7d753c1bedac..1e1fbc4d1a87 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -1028,8 +1028,11 @@ public function testSetBadSegmentSilent(): void public function testBasedNoIndex(): void { - $_SERVER['HTTP_HOST'] = 'example.com'; - $_SERVER['REQUEST_URI'] = '/ci/v4/controller/method'; + $_SERVER['REQUEST_URI'] = '/ci/v4/controller/method'; + $_SERVER['SCRIPT_NAME'] = '/ci/v4/index.php'; + $_SERVER['QUERY_STRING'] = ''; + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['PATH_INFO'] = '/controller/method'; $this->resetServices(); @@ -1046,20 +1049,24 @@ public function testBasedNoIndex(): void 'http://example.com/ci/v4/controller/method', (string) $request->getUri() ); - $this->assertSame('ci/v4/controller/method', $request->getUri()->getPath()); + $this->assertSame('/ci/v4/controller/method', $request->getUri()->getPath()); + $this->assertSame('controller/method', $request->getUri()->getRoutePath()); // standalone $uri = new URI('http://example.com/ci/v4/controller/method'); $this->assertSame('http://example.com/ci/v4/controller/method', (string) $uri); $this->assertSame('/ci/v4/controller/method', $uri->getPath()); - $this->assertSame($uri->getPath(), '/' . $request->getUri()->getPath()); + $this->assertSame($uri->getPath(), $request->getUri()->getPath()); } public function testBasedWithIndex(): void { - $_SERVER['HTTP_HOST'] = 'example.com'; - $_SERVER['REQUEST_URI'] = '/ci/v4/index.php/controller/method'; + $_SERVER['REQUEST_URI'] = '/ci/v4/index.php/controller/method'; + $_SERVER['SCRIPT_NAME'] = '/ci/v4/index.php'; + $_SERVER['QUERY_STRING'] = ''; + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['PATH_INFO'] = '/controller/method'; $this->resetServices(); @@ -1077,7 +1084,7 @@ public function testBasedWithIndex(): void (string) $request->getUri() ); $this->assertSame( - 'ci/v4/index.php/controller/method', + '/ci/v4/index.php/controller/method', $request->getUri()->getPath() ); @@ -1089,26 +1096,26 @@ public function testBasedWithIndex(): void ); $this->assertSame('/ci/v4/index.php/controller/method', $uri->getPath()); - $this->assertSame($uri->getPath(), '/' . $request->getUri()->getPath()); + $this->assertSame($uri->getPath(), $request->getUri()->getPath()); } public function testForceGlobalSecureRequests(): void { $this->resetServices(); - $_SERVER['HTTP_HOST'] = 'example.com'; - $_SERVER['REQUEST_URI'] = '/ci/v4/controller/method'; + $_SERVER['REQUEST_URI'] = '/ci/v4/controller/method'; + $_SERVER['SCRIPT_NAME'] = '/ci/v4/index.php'; + $_SERVER['QUERY_STRING'] = ''; + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['PATH_INFO'] = '/controller/method'; $config = new App(); $config->baseURL = 'http://example.com/ci/v4'; - $config->indexPage = 'index.php'; + $config->indexPage = ''; $config->forceGlobalSecureRequests = true; - Factories::injectMock('config', 'App', $config); - $uri = new URI('http://example.com/ci/v4/controller/method'); - $request = new IncomingRequest($config, $uri, 'php://input', new UserAgent()); - + $request = Services::request($config); Services::injectMock('request', $request); // Detected by request From fc0a1b8f996360e98eab7a6d97cc2584418a75dd Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 20 Feb 2023 17:57:23 +0900 Subject: [PATCH 432/485] refactor: URL helper site_url(), base_url(), current_url(), uri_string() --- system/HTTP/SiteURI.php | 83 ++++++++++ system/Helpers/url_helper.php | 145 ++---------------- .../Helpers/URLHelper/CurrentUrlTest.php | 82 +++++----- .../system/Helpers/URLHelper/MiscUrlTest.php | 39 ++--- .../system/Helpers/URLHelper/SiteUrlTest.php | 58 +++++-- 5 files changed, 209 insertions(+), 198 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index adf873bfb902..72196e1bc365 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -363,4 +363,87 @@ protected function applyParts(array $parts): void $this->password = $parts['pass']; } } + + /** + * For baser_url() helper. + * + * @param array|string $relativePath URI string or array of URI segments + * @param string|null $scheme URI scheme. E.g., http, ftp + */ + public function baseUrl($relativePath = '', ?string $scheme = null): string + { + $relativePath = $this->stringifyRelativePath($relativePath); + + $config = clone config(App::class); + $config->indexPage = ''; + + $host = $this->getHost(); + + $uri = new self($config, $relativePath, $host, $scheme); + + return URI::createURIString( + $uri->getScheme(), + $uri->getAuthority(), + $this->adjustPathTrailingSlash($uri, $relativePath), + $uri->getQuery(), + $uri->getFragment() + ); + } + + /** + * @param array|string $relativePath URI string or array of URI segments + */ + private function stringifyRelativePath($relativePath): string + { + if (is_array($relativePath)) { + $relativePath = implode('/', $relativePath); + } + + return $relativePath; + } + + /** + * For site_url() helper. + * + * @param array|string $relativePath URI string or array of URI segments + * @param string|null $scheme URI scheme. E.g., http, ftp + * @param App|null $config Alternate configuration to use + */ + public function siteUrl($relativePath = '', ?string $scheme = null, ?App $config = null): string + { + $relativePath = $this->stringifyRelativePath($relativePath); + + // Check current host. + $host = $config === null ? $this->getHost() : null; + + $config ??= config(App::class); + + $uri = new self($config, $relativePath, $host, $scheme); + + // Adjust path + $path = $this->adjustPathTrailingSlash($uri, $relativePath); + if ($config->indexPage !== '' && $relativePath === '') { + $path = rtrim($path, '/'); + } + + return URI::createURIString( + $uri->getScheme(), + $uri->getAuthority(), + $path, + $uri->getQuery(), + $uri->getFragment() + ); + } + + private function adjustPathTrailingSlash(self $uri, string $relativePath): string + { + $parts = parse_url($this->getBaseURL() . $relativePath); + $path = $parts['path'] ?? ''; + + if (substr($path, -1) === '/' && substr($uri->getPath(), -1) !== '/') { + return $uri->getPath() . '/'; + } + + return $uri->getPath(); + } } diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php index c6c810b66da6..b8c1a687e6bb 100644 --- a/system/Helpers/url_helper.php +++ b/system/Helpers/url_helper.php @@ -10,8 +10,8 @@ */ use CodeIgniter\HTTP\CLIRequest; -use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\SiteURI; use CodeIgniter\HTTP\URI; use CodeIgniter\Router\Exceptions\RouterException; use Config\App; @@ -19,92 +19,6 @@ // CodeIgniter URL Helpers -if (! function_exists('_get_uri')) { - /** - * Used by the other URL functions to build a framework-specific URI - * based on $request->getUri()->getBaseURL() and the App config. - * - * @internal Outside the framework this should not be used directly. - * - * @param array|string $relativePath URI string or array of URI segments. - * May include queries or fragments. - * @param App|null $config Alternative Config to use - * - * @throws HTTPException For invalid paths. - * @throws InvalidArgumentException For invalid config. - */ - function _get_uri($relativePath = '', ?App $config = null): URI - { - $appConfig = null; - if ($config === null) { - /** @var App $appConfig */ - $appConfig = config(App::class); - - if ($appConfig->baseURL === '') { - throw new InvalidArgumentException( - '_get_uri() requires a valid baseURL.' - ); - } - } elseif ($config->baseURL === '') { - throw new InvalidArgumentException( - '_get_uri() requires a valid baseURL.' - ); - } - - // Convert array of segments to a string - if (is_array($relativePath)) { - $relativePath = implode('/', $relativePath); - } - - // If a full URI was passed then convert it - if (strpos($relativePath, '://') !== false) { - $full = new URI($relativePath); - $relativePath = URI::createURIString( - null, - null, - $full->getPath(), - $full->getQuery(), - $full->getFragment() - ); - } - - $relativePath = URI::removeDotSegments($relativePath); - - $request = Services::request(); - - if ($config === null) { - $baseURL = $request instanceof CLIRequest - ? rtrim($appConfig->baseURL, '/ ') . '/' - // Use the current baseURL for multiple domain support - : $request->getUri()->getBaseURL(); - - $config = $appConfig; - } else { - $baseURL = rtrim($config->baseURL, '/ ') . '/'; - } - - // Check for an index page - $indexPage = ''; - if ($config->indexPage !== '') { - $indexPage = $config->indexPage; - - // Check if we need a separator - if ($relativePath !== '' && $relativePath[0] !== '/' && $relativePath[0] !== '?') { - $indexPage .= '/'; - } - } - - $uri = new URI($baseURL . $indexPage . $relativePath); - - // Check if the baseURL scheme needs to be coerced into its secure version - if ($config->forceGlobalSecureRequests && $uri->getScheme() === 'http') { - $uri->setScheme('https'); - } - - return $uri; - } -} - if (! function_exists('site_url')) { /** * Returns a site URL as defined by the App config. @@ -115,22 +29,12 @@ function _get_uri($relativePath = '', ?App $config = null): URI */ function site_url($relativePath = '', ?string $scheme = null, ?App $config = null): string { - $uri = _get_uri($relativePath, $config); - - $uriString = URI::createURIString( - $scheme ?? $uri->getScheme(), - $uri->getAuthority(), - $uri->getPath(), - $uri->getQuery(), - $uri->getFragment() - ); - - // For protocol-relative links - if ($scheme === '') { - $uriString = '//' . $uriString; - } + $currentURI = Services::request()->getUri(); + + assert($currentURI instanceof SiteURI); - return $uriString; + // @TODO supprot protocol-relative links + return $currentURI->siteUrl($relativePath, $scheme, $config); } } @@ -144,18 +48,11 @@ function site_url($relativePath = '', ?string $scheme = null, ?App $config = nul */ function base_url($relativePath = '', ?string $scheme = null): string { - /** @var App $config */ - $config = clone config(App::class); - - // Use the current baseURL for multiple domain support - $request = Services::request(); - $config->baseURL = $request instanceof CLIRequest - ? rtrim($config->baseURL, '/ ') . '/' - : $request->getUri()->getBaseURL(); + $currentURI = Services::request()->getUri(); - $config->indexPage = ''; + assert($currentURI instanceof SiteURI); - return site_url($relativePath, $scheme, $config); + return $currentURI->baseUrl($relativePath, $scheme); } } @@ -173,18 +70,7 @@ function current_url(bool $returnObject = false, ?IncomingRequest $request = nul { $request ??= Services::request(); /** @var CLIRequest|IncomingRequest $request */ - $routePath = $request->getPath(); - $currentURI = $request->getUri(); - - // Append queries and fragments - if ($query = $currentURI->getQuery()) { - $query = '?' . $query; - } - if ($fragment = $currentURI->getFragment()) { - $fragment = '#' . $fragment; - } - - $uri = _get_uri($routePath . $query . $fragment); + $uri = $request->getUri(); return $returnObject ? $uri : URI::createURIString($uri->getScheme(), $uri->getAuthority(), $uri->getPath()); } @@ -220,10 +106,13 @@ function previous_url(bool $returnObject = false) */ function uri_string(): string { - // The value of Services::request()->getUri()->getPath() is overridden - // by IncomingRequest constructor. If we use it here, the current tests - // in CurrentUrlTest will fail. - return ltrim(Services::request()->getPath(), '/'); + // The value of Services::request()->getUri()->getPath() returns + // full URI path. + $uri = Services::request()->getUri(); + + $path = $uri instanceof SiteURI ? $uri->getRoutePath() : $uri->getPath(); + + return ltrim($path, '/'); } } diff --git a/tests/system/Helpers/URLHelper/CurrentUrlTest.php b/tests/system/Helpers/URLHelper/CurrentUrlTest.php index 6f908053b6a6..fd5f7dda9ea7 100644 --- a/tests/system/Helpers/URLHelper/CurrentUrlTest.php +++ b/tests/system/Helpers/URLHelper/CurrentUrlTest.php @@ -13,7 +13,11 @@ use CodeIgniter\Config\Factories; use CodeIgniter\Config\Services; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\SiteURIFactory; use CodeIgniter\HTTP\URI; +use CodeIgniter\HTTP\UserAgent; +use CodeIgniter\Superglobals; use CodeIgniter\Test\CIUnitTestCase; use Config\App; @@ -42,7 +46,6 @@ protected function setUp(): void $this->config = new App(); $this->config->baseURL = 'http://example.com/'; $this->config->indexPage = 'index.php'; - Factories::injectMock('config', 'App', $this->config); $_SERVER['HTTP_HOST'] = 'example.com'; $_SERVER['REQUEST_URI'] = '/'; @@ -63,9 +66,7 @@ public function testCurrentURLReturnsBasicURL(): void $this->config->baseURL = 'http://example.com/public'; - // URI object are updated in IncomingRequest constructor. - $request = Services::incomingrequest($this->config); - Services::injectMock('request', $request); + $this->createRequest($this->config); $this->assertSame('http://example.com/public/index.php/', current_url()); } @@ -79,9 +80,28 @@ public function testCurrentURLReturnsAllowedHostname(): void $this->config->baseURL = 'http://example.com/public'; $this->config->allowedHostnames = ['www.example.jp']; + $this->createRequest($this->config); + $this->assertSame('http://www.example.jp/public/index.php/', current_url()); } + private function createRequest(?App $config = null, $body = null, ?string $path = null): void + { + $config ??= new App(); + + $factory = new SiteURIFactory($config, new Superglobals()); + $uri = $factory->createFromGlobals(); + + if ($path !== null) { + $uri->setPath($path); + } + + $request = new IncomingRequest($config, $uri, $body, new UserAgent()); + Services::injectMock('request', $request); + + Factories::injectMock('config', 'App', $config); + } + public function testCurrentURLReturnsBaseURLIfNotAllowedHostname(): void { $_SERVER['HTTP_HOST'] = 'invalid.example.org'; @@ -91,6 +111,8 @@ public function testCurrentURLReturnsBaseURLIfNotAllowedHostname(): void $this->config->baseURL = 'http://example.com/public'; $this->config->allowedHostnames = ['www.example.jp']; + $this->createRequest($this->config); + $this->assertSame('http://example.com/public/index.php/', current_url()); } @@ -99,6 +121,8 @@ public function testCurrentURLReturnsObject(): void // Since we're on a CLI, we must provide our own URI $this->config->baseURL = 'http://example.com/public'; + $this->createRequest($this->config); + $url = current_url(true); $this->assertInstanceOf(URI::class, $url); @@ -111,11 +135,9 @@ public function testCurrentURLEquivalence(): void $_SERVER['REQUEST_URI'] = '/public'; $_SERVER['SCRIPT_NAME'] = '/index.php'; - // Since we're on a CLI, we must provide our own URI - Factories::injectMock('config', 'App', $this->config); + $this->config->indexPage = ''; - $request = Services::request($this->config); - Services::injectMock('request', $request); + $this->createRequest($this->config); $this->assertSame(site_url(uri_string()), current_url()); } @@ -128,16 +150,14 @@ public function testCurrentURLInSubfolder(): void // Since we're on a CLI, we must provide our own URI $this->config->baseURL = 'http://example.com/foo/public'; - Factories::injectMock('config', 'App', $this->config); - $request = Services::request($this->config); - Services::injectMock('request', $request); + $this->createRequest($this->config); $this->assertSame('http://example.com/foo/public/index.php/bar', current_url()); $this->assertSame('http://example.com/foo/public/index.php/bar?baz=quip', (string) current_url(true)); $uri = current_url(true); - $this->assertSame('foo', $uri->getSegment(1)); + $this->assertSame('bar', $uri->getSegment(1)); $this->assertSame('example.com', $uri->getHost()); $this->assertSame('http', $uri->getScheme()); } @@ -149,19 +169,16 @@ public function testCurrentURLWithPortInSubfolder(): void $_SERVER['REQUEST_URI'] = '/foo/public/bar?baz=quip'; $_SERVER['SCRIPT_NAME'] = '/foo/public/index.php'; - // Since we're on a CLI, we must provide our own URI $this->config->baseURL = 'http://example.com:8080/foo/public'; - Factories::injectMock('config', 'App', $this->config); - $request = Services::request($this->config); - Services::injectMock('request', $request); + $this->createRequest($this->config); $this->assertSame('http://example.com:8080/foo/public/index.php/bar', current_url()); $this->assertSame('http://example.com:8080/foo/public/index.php/bar?baz=quip', (string) current_url(true)); $uri = current_url(true); - $this->assertSame(['foo', 'public', 'index.php', 'bar'], $uri->getSegments()); - $this->assertSame('foo', $uri->getSegment(1)); + $this->assertSame(['bar'], $uri->getSegments()); + $this->assertSame('bar', $uri->getSegment(1)); $this->assertSame('example.com', $uri->getHost()); $this->assertSame('http', $uri->getScheme()); $this->assertSame(8080, $uri->getPort()); @@ -172,8 +189,9 @@ public function testUriString(): void $_SERVER['HTTP_HOST'] = 'example.com'; $_SERVER['REQUEST_URI'] = '/assets/image.jpg'; - $uri = 'http://example.com/assets/image.jpg'; - $this->setService($uri); + $this->config->indexPage = ''; + + $this->createRequest($this->config); $this->assertSame('assets/image.jpg', uri_string()); } @@ -192,18 +210,17 @@ public function testUriStringNoTrailingSlash(): void $_SERVER['HTTP_HOST'] = 'example.com'; $_SERVER['REQUEST_URI'] = '/assets/image.jpg'; - $this->config->baseURL = 'http://example.com'; + $this->config->baseURL = 'http://example.com'; + $this->config->indexPage = ''; - $uri = 'http://example.com/assets/image.jpg'; - $this->setService($uri); + $this->createRequest($this->config); $this->assertSame('assets/image.jpg', uri_string()); } public function testUriStringEmpty(): void { - $uri = 'http://example.com/'; - $this->setService($uri); + $this->createRequest($this->config); $this->assertSame('', uri_string()); } @@ -215,8 +232,7 @@ public function testUriStringSubfolderAbsolute(): void $this->config->baseURL = 'http://example.com/subfolder/'; - $uri = 'http://example.com/subfolder/assets/image.jpg'; - $this->setService($uri); + $this->createRequest($this->config); $this->assertSame('subfolder/assets/image.jpg', uri_string()); } @@ -229,8 +245,7 @@ public function testUriStringSubfolderRelative(): void $this->config->baseURL = 'http://example.com/subfolder/'; - $uri = 'http://example.com/subfolder/assets/image.jpg'; - $this->setService($uri); + $this->createRequest($this->config); $this->assertSame('assets/image.jpg', uri_string()); } @@ -284,8 +299,7 @@ public function testUrlIs(string $currentPath, string $testPath, bool $expected) $_SERVER['HTTP_HOST'] = 'example.com'; $_SERVER['REQUEST_URI'] = '/' . $currentPath; - $uri = 'http://example.com/' . $currentPath; - $this->setService($uri); + $this->createRequest($this->config); $this->assertSame($expected, url_is($testPath)); } @@ -300,8 +314,7 @@ public function testUrlIsNoIndex(string $currentPath, string $testPath, bool $ex $this->config->indexPage = ''; - $uri = 'http://example.com/' . $currentPath; - $this->setService($uri); + $this->createRequest($this->config); $this->assertSame($expected, url_is($testPath)); } @@ -317,8 +330,7 @@ public function testUrlIsWithSubfolder(string $currentPath, string $testPath, bo $this->config->baseURL = 'http://example.com/subfolder/'; - $uri = 'http://example.com/subfolder/' . $currentPath; - $this->setService($uri); + $this->createRequest($this->config); $this->assertSame($expected, url_is($testPath)); } diff --git a/tests/system/Helpers/URLHelper/MiscUrlTest.php b/tests/system/Helpers/URLHelper/MiscUrlTest.php index 7af14d169f16..c26e9e7fbf9b 100644 --- a/tests/system/Helpers/URLHelper/MiscUrlTest.php +++ b/tests/system/Helpers/URLHelper/MiscUrlTest.php @@ -13,7 +13,9 @@ use CodeIgniter\Config\Factories; use CodeIgniter\Config\Services; -use CodeIgniter\HTTP\URI; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\SiteURIFactory; +use CodeIgniter\HTTP\UserAgent; use CodeIgniter\Router\Exceptions\RouterException; use CodeIgniter\Test\CIUnitTestCase; use Config\App; @@ -40,7 +42,6 @@ protected function setUp(): void $this->config = new App(); $this->config->baseURL = 'http://example.com/'; $this->config->indexPage = 'index.php'; - Factories::injectMock('config', 'App', $this->config); } protected function tearDown(): void @@ -61,19 +62,21 @@ public function testPreviousURLUsesSessionFirst(): void $this->config->baseURL = 'http://example.com/public'; $uri = 'http://example.com/public'; - $this->setRequest($uri); + $this->createRequest($uri); $this->assertSame($uri2, previous_url()); } - private function setRequest(string $uri): void + private function createRequest(string $uri): void { - $uri = new URI($uri); - Services::injectMock('uri', $uri); + $factory = new SiteURIFactory($_SERVER, $this->config); + + $uri = $factory->createFromString($uri); - // Since we're on a CLI, we must provide our own URI - $request = Services::request($this->config); + $request = new IncomingRequest($this->config, $uri, null, new UserAgent()); Services::injectMock('request', $request); + + Factories::injectMock('config', 'App', $this->config); } public function testPreviousURLUsesRefererIfNeeded(): void @@ -85,7 +88,7 @@ public function testPreviousURLUsesRefererIfNeeded(): void $this->config->baseURL = 'http://example.com/public'; $uri = 'http://example.com/public'; - $this->setRequest($uri); + $this->createRequest($uri); $this->assertSame($uri1, previous_url()); } @@ -95,7 +98,7 @@ public function testPreviousURLUsesRefererIfNeeded(): void public function testIndexPage(): void { $uri = 'http://example.com/'; - $this->setRequest($uri); + $this->createRequest($uri); $this->assertSame('index.php', index_page()); } @@ -105,7 +108,7 @@ public function testIndexPageAlt(): void $this->config->indexPage = 'banana.php'; $uri = 'http://example.com/'; - $this->setRequest($uri); + $this->createRequest($uri); $this->assertSame('banana.php', index_page($this->config)); } @@ -166,7 +169,7 @@ public static function provideAnchor(): iterable public function testAnchor($expected = '', $uri = '', $title = '', $attributes = ''): void { $uriString = 'http://example.com/'; - $this->setRequest($uriString); + $this->createRequest($uriString); $this->assertSame($expected, anchor($uri, $title, $attributes, $this->config)); } @@ -233,7 +236,7 @@ public function testAnchorNoindex($expected = '', $uri = '', $title = '', $attri $this->config->indexPage = ''; $uriString = 'http://example.com/'; - $this->setRequest($uriString); + $this->createRequest($uriString); $this->assertSame($expected, anchor($uri, $title, $attributes, $this->config)); } @@ -290,7 +293,7 @@ public function testAnchorTargetted($expected = '', $uri = '', $title = '', $att $this->config->indexPage = ''; $uriString = 'http://example.com/'; - $this->setRequest($uriString); + $this->createRequest($uriString); $this->assertSame($expected, anchor($uri, $title, $attributes, $this->config)); } @@ -334,7 +337,7 @@ public static function provideAnchorExamples(): iterable public function testAnchorExamples($expected = '', $uri = '', $title = '', $attributes = ''): void { $uriString = 'http://example.com/'; - $this->setRequest($uriString); + $this->createRequest($uriString); $this->assertSame($expected, anchor($uri, $title, $attributes, $this->config)); } @@ -392,7 +395,7 @@ public static function provideAnchorPopup(): iterable public function testAnchorPopup($expected = '', $uri = '', $title = '', $attributes = false): void { $uriString = 'http://example.com/'; - $this->setRequest($uriString); + $this->createRequest($uriString); $this->assertSame($expected, anchor_popup($uri, $title, $attributes, $this->config)); } @@ -431,7 +434,7 @@ public static function provideMailto(): iterable public function testMailto($expected = '', $email = '', $title = '', $attributes = ''): void { $uriString = 'http://example.com/'; - $this->setRequest($uriString); + $this->createRequest($uriString); $this->assertSame($expected, mailto($email, $title, $attributes)); } @@ -470,7 +473,7 @@ public static function provideSafeMailto(): iterable public function testSafeMailto($expected = '', $email = '', $title = '', $attributes = ''): void { $uriString = 'http://example.com/'; - $this->setRequest($uriString); + $this->createRequest($uriString); $this->assertSame($expected, safe_mailto($email, $title, $attributes)); } diff --git a/tests/system/Helpers/URLHelper/SiteUrlTest.php b/tests/system/Helpers/URLHelper/SiteUrlTest.php index cafcb2f3ee6a..a28a588cbc5f 100644 --- a/tests/system/Helpers/URLHelper/SiteUrlTest.php +++ b/tests/system/Helpers/URLHelper/SiteUrlTest.php @@ -13,7 +13,10 @@ use CodeIgniter\Config\Factories; use CodeIgniter\Config\Services; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\SiteURIFactory; use CodeIgniter\HTTP\URI; +use CodeIgniter\HTTP\UserAgent; use CodeIgniter\Test\CIUnitTestCase; use Config\App; @@ -39,7 +42,6 @@ protected function setUp(): void Services::reset(true); $this->config = new App(); - Factories::injectMock('config', 'App', $this->config); } protected function tearDown(): void @@ -49,6 +51,23 @@ protected function tearDown(): void $_SERVER = []; } + private function createRequest(?App $config = null, $body = null, ?string $path = null): void + { + $config ??= new App(); + + $factory = new SiteURIFactory($_SERVER, $config); + $uri = $factory->createFromGlobals(); + + if ($path !== null) { + $uri->setPath($path); + } + + $request = new IncomingRequest($config, $uri, $body, new UserAgent()); + Services::injectMock('request', $request); + + Factories::injectMock('config', 'App', $config); + } + /** * Takes a multitude of various config input and verifies * that base_url() and site_url() return the expected result. @@ -77,6 +96,8 @@ public function testUrls( $this->config->indexPage = $indexPage; $this->config->forceGlobalSecureRequests = $secure; + $this->createRequest($this->config); + $this->assertSame($expectedSiteUrl, site_url($path, $scheme, $this->config)); $this->assertSame($expectedBaseUrl, base_url($path, $scheme)); } @@ -333,11 +354,15 @@ public function testBaseURLDiscovery(): void $_SERVER['HTTP_HOST'] = 'example.com'; $_SERVER['REQUEST_URI'] = '/test'; + $this->createRequest($this->config); + $this->assertSame('http://example.com/', base_url()); $_SERVER['HTTP_HOST'] = 'example.com'; $_SERVER['REQUEST_URI'] = '/test/page'; + $this->createRequest($this->config); + $this->assertSame('http://example.com/', base_url()); $this->assertSame('http://example.com/profile', base_url('profile')); } @@ -348,11 +373,17 @@ public function testBaseURLService(): void $_SERVER['REQUEST_URI'] = '/ci/v4/x/y'; $this->config->baseURL = 'http://example.com/ci/v4/'; - $request = Services::request($this->config); - Services::injectMock('request', $request); - $this->assertSame('http://example.com/ci/v4/index.php/controller/method', site_url('controller/method', null, $this->config)); - $this->assertSame('http://example.com/ci/v4/controller/method', base_url('controller/method', null)); + $this->createRequest($this->config); + + $this->assertSame( + 'http://example.com/ci/v4/index.php/controller/method', + site_url('controller/method', null, $this->config) + ); + $this->assertSame( + 'http://example.com/ci/v4/controller/method', + base_url('controller/method', null) + ); } public function testBaseURLWithCLIRequest(): void @@ -360,8 +391,8 @@ public function testBaseURLWithCLIRequest(): void unset($_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI']); $this->config->baseURL = 'http://example.com/'; - $request = Services::clirequest($this->config); - Services::injectMock('request', $request); + + $this->createRequest($this->config); $this->assertSame( 'http://example.com/index.php/controller/method', @@ -381,11 +412,8 @@ public function testSiteURLWithAllowedHostname(): void $this->config->baseURL = 'http://example.com/public/'; $this->config->allowedHostnames = ['www.example.jp']; - Services::injectMock('config', $this->config); - // URI object are updated in IncomingRequest constructor. - $request = Services::incomingrequest($this->config); - Services::injectMock('request', $request); + $this->createRequest($this->config); $this->assertSame( 'http://www.example.jp/public/index.php/controller/method', @@ -402,9 +430,7 @@ public function testSiteURLWithAltConfig(): void $this->config->baseURL = 'http://example.com/public/'; $this->config->allowedHostnames = ['www.example.jp']; - // URI object are updated in IncomingRequest constructor. - $request = Services::incomingrequest($this->config); - Services::injectMock('request', $request); + $this->createRequest($this->config); $altConfig = clone $this->config; $altConfig->baseURL = 'http://alt.example.com/public/'; @@ -424,9 +450,7 @@ public function testBaseURLWithAllowedHostname(): void $this->config->baseURL = 'http://example.com/public/'; $this->config->allowedHostnames = ['www.example.jp']; - // URI object are updated in IncomingRequest constructor. - $request = Services::incomingrequest($this->config); - Services::injectMock('request', $request); + $this->createRequest($this->config); $this->assertSame( 'http://www.example.jp/public/controller/method', From 468b8b5f26f42aaf673d265d2cc5e31be46e60b1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 21 Feb 2023 15:17:33 +0900 Subject: [PATCH 433/485] test: fix incorrect REQUEST_URI The public is a folder, so should be end with `/`. --- .../Helpers/URLHelper/CurrentUrlTest.php | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/system/Helpers/URLHelper/CurrentUrlTest.php b/tests/system/Helpers/URLHelper/CurrentUrlTest.php index fd5f7dda9ea7..a28fa78b9b98 100644 --- a/tests/system/Helpers/URLHelper/CurrentUrlTest.php +++ b/tests/system/Helpers/URLHelper/CurrentUrlTest.php @@ -61,10 +61,10 @@ protected function tearDown(): void public function testCurrentURLReturnsBasicURL(): void { - $_SERVER['REQUEST_URI'] = '/public'; + $_SERVER['REQUEST_URI'] = '/public/'; $_SERVER['SCRIPT_NAME'] = '/public/index.php'; - $this->config->baseURL = 'http://example.com/public'; + $this->config->baseURL = 'http://example.com/public/'; $this->createRequest($this->config); @@ -74,10 +74,10 @@ public function testCurrentURLReturnsBasicURL(): void public function testCurrentURLReturnsAllowedHostname(): void { $_SERVER['HTTP_HOST'] = 'www.example.jp'; - $_SERVER['REQUEST_URI'] = '/public'; + $_SERVER['REQUEST_URI'] = '/public/'; $_SERVER['SCRIPT_NAME'] = '/public/index.php'; - $this->config->baseURL = 'http://example.com/public'; + $this->config->baseURL = 'http://example.com/public/'; $this->config->allowedHostnames = ['www.example.jp']; $this->createRequest($this->config); @@ -105,10 +105,10 @@ private function createRequest(?App $config = null, $body = null, ?string $path public function testCurrentURLReturnsBaseURLIfNotAllowedHostname(): void { $_SERVER['HTTP_HOST'] = 'invalid.example.org'; - $_SERVER['REQUEST_URI'] = '/public'; + $_SERVER['REQUEST_URI'] = '/public/'; $_SERVER['SCRIPT_NAME'] = '/public/index.php'; - $this->config->baseURL = 'http://example.com/public'; + $this->config->baseURL = 'http://example.com/public/'; $this->config->allowedHostnames = ['www.example.jp']; $this->createRequest($this->config); @@ -118,8 +118,7 @@ public function testCurrentURLReturnsBaseURLIfNotAllowedHostname(): void public function testCurrentURLReturnsObject(): void { - // Since we're on a CLI, we must provide our own URI - $this->config->baseURL = 'http://example.com/public'; + $this->config->baseURL = 'http://example.com/public/'; $this->createRequest($this->config); @@ -132,7 +131,7 @@ public function testCurrentURLReturnsObject(): void public function testCurrentURLEquivalence(): void { $_SERVER['HTTP_HOST'] = 'example.com'; - $_SERVER['REQUEST_URI'] = '/public'; + $_SERVER['REQUEST_URI'] = '/public/'; $_SERVER['SCRIPT_NAME'] = '/index.php'; $this->config->indexPage = ''; @@ -148,8 +147,7 @@ public function testCurrentURLInSubfolder(): void $_SERVER['REQUEST_URI'] = '/foo/public/bar?baz=quip'; $_SERVER['SCRIPT_NAME'] = '/foo/public/index.php'; - // Since we're on a CLI, we must provide our own URI - $this->config->baseURL = 'http://example.com/foo/public'; + $this->config->baseURL = 'http://example.com/foo/public/'; $this->createRequest($this->config); @@ -169,7 +167,7 @@ public function testCurrentURLWithPortInSubfolder(): void $_SERVER['REQUEST_URI'] = '/foo/public/bar?baz=quip'; $_SERVER['SCRIPT_NAME'] = '/foo/public/index.php'; - $this->config->baseURL = 'http://example.com:8080/foo/public'; + $this->config->baseURL = 'http://example.com:8080/foo/public/'; $this->createRequest($this->config); @@ -210,7 +208,7 @@ public function testUriStringNoTrailingSlash(): void $_SERVER['HTTP_HOST'] = 'example.com'; $_SERVER['REQUEST_URI'] = '/assets/image.jpg'; - $this->config->baseURL = 'http://example.com'; + $this->config->baseURL = 'http://example.com/'; $this->config->indexPage = ''; $this->createRequest($this->config); From c29c172385290e006154538b1cb19fae020a0b32 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 21 Feb 2023 15:18:27 +0900 Subject: [PATCH 434/485] test: update FormHelperTest --- tests/system/Helpers/FormHelperTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php index 1f222aff850f..cbd361875286 100644 --- a/tests/system/Helpers/FormHelperTest.php +++ b/tests/system/Helpers/FormHelperTest.php @@ -11,7 +11,7 @@ namespace CodeIgniter\Helpers; -use CodeIgniter\HTTP\URI; +use CodeIgniter\HTTP\SiteURI; use CodeIgniter\Test\CIUnitTestCase; use Config\App; use Config\Filters; @@ -35,12 +35,12 @@ protected function setUp(): void private function setRequest(): void { - $uri = new URI('http://example.com/'); - Services::injectMock('uri', $uri); - $config = new App(); $config->indexPage = 'index.php'; + $uri = new SiteURI($config); + Services::injectMock('uri', $uri); + $request = Services::request($config); Services::injectMock('request', $request); } From e68379ef52643887fd1e566e0c81a3970900ac0d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 21 Feb 2023 16:02:03 +0900 Subject: [PATCH 435/485] test: fix failed test In GitHub Actions: Error: Call to undefined method CodeIgniter\HTTP\URI::siteUrl() --- tests/system/Helpers/HTMLHelperTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/system/Helpers/HTMLHelperTest.php b/tests/system/Helpers/HTMLHelperTest.php index 1f8485393b3f..0d9452640730 100755 --- a/tests/system/Helpers/HTMLHelperTest.php +++ b/tests/system/Helpers/HTMLHelperTest.php @@ -39,6 +39,8 @@ protected function setUp(): void { parent::setUp(); + $this->resetServices(); + helper('html'); $this->tracks = [ From 8464087e299082f8b64fc7fab7509ffad69c03d5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Feb 2023 16:16:40 +0900 Subject: [PATCH 436/485] test: update URLs in assertions --- tests/system/CodeIgniterTest.php | 4 ++-- tests/system/HTTP/ResponseTest.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 55392e0aebc4..5fae784b51c5 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -450,7 +450,7 @@ public function testRunForceSecure(): void $response = $codeigniter->run(null, true); - $this->assertSame('https://example.com/', $response->header('Location')->getValue()); + $this->assertSame('https://example.com/index.php/', $response->header('Location')->getValue()); } public function testRunRedirectionWithNamed(): void @@ -618,7 +618,7 @@ public function testStoresPreviousURL(): void ob_get_clean(); $this->assertArrayHasKey('_ci_previous_url', $_SESSION); - $this->assertSame('http://example.com/index.php', $_SESSION['_ci_previous_url']); + $this->assertSame('http://example.com/index.php/', $_SESSION['_ci_previous_url']); } public function testNotStoresPreviousURL(): void diff --git a/tests/system/HTTP/ResponseTest.php b/tests/system/HTTP/ResponseTest.php index 50d515c3c144..e08dc1de62a3 100644 --- a/tests/system/HTTP/ResponseTest.php +++ b/tests/system/HTTP/ResponseTest.php @@ -172,7 +172,7 @@ public function testSetLink(): void $response->setLink($pager); $this->assertSame( - '; rel="first",; rel="prev",; rel="next",; rel="last"', + '; rel="first",; rel="prev",; rel="next",; rel="last"', $response->header('Link')->getValue() ); @@ -180,7 +180,7 @@ public function testSetLink(): void $response->setLink($pager); $this->assertSame( - '; rel="next",; rel="last"', + '; rel="next",; rel="last"', $response->header('Link')->getValue() ); @@ -188,7 +188,7 @@ public function testSetLink(): void $response->setLink($pager); $this->assertSame( - '; rel="first",; rel="prev"', + '; rel="first",; rel="prev"', $response->header('Link')->getValue() ); } From 76dfd39eb6ae52157fca2e7b606d651a2d629b53 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 23 Feb 2023 16:28:16 +0900 Subject: [PATCH 437/485] feat: add Services::siteurifactory() --- system/Config/BaseService.php | 3 +++ system/Config/Services.php | 22 +++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index e4e82c67298b..474e5e06cfa3 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -37,6 +37,7 @@ use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\HTTP\SiteURIFactory; use CodeIgniter\HTTP\URI; use CodeIgniter\Images\Handlers\BaseHandler; use CodeIgniter\Language\Language; @@ -47,6 +48,7 @@ use CodeIgniter\Router\Router; use CodeIgniter\Security\Security; use CodeIgniter\Session\Session; +use CodeIgniter\Superglobals; use CodeIgniter\Throttle\Throttler; use CodeIgniter\Typography\Typography; use CodeIgniter\Validation\ValidationInterface; @@ -123,6 +125,7 @@ * @method static RouteCollection routes($getShared = true) * @method static Security security(App $config = null, $getShared = true) * @method static Session session(App $config = null, $getShared = true) + * @method static SiteURIFactory siteurifactory(App $config = null, Superglobals $superglobals = null, $getShared = true) * @method static Throttler throttler($getShared = true) * @method static Timer timer($getShared = true) * @method static Toolbar toolbar(ConfigToolbar $config = null, $getShared = true) diff --git a/system/Config/Services.php b/system/Config/Services.php index 2e1e7879c087..6355b92ecc63 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -695,6 +695,26 @@ public static function session(?SessionConfig $config = null, bool $getShared = return $session; } + /** + * The Factory for SiteURI. + * + * @return SiteURIFactory + */ + public static function siteurifactory( + ?App $config = null, + ?Superglobals $superglobals = null, + bool $getShared = true + ) { + if ($getShared) { + return static::getSharedInstance('siteurifactory', $config, $superglobals); + } + + $config ??= config('App'); + $superglobals ??= new Superglobals(); + + return new SiteURIFactory($config, $superglobals); + } + /** * The Throttler class provides a simple method for implementing * rate limiting in your applications. @@ -756,7 +776,7 @@ public static function uri(?string $uri = null, bool $getShared = true) if ($uri === null) { $appConfig = config(App::class); - $factory = new SiteURIFactory($appConfig, new Superglobals()); + $factory = AppServices::siteurifactory($appConfig, new Superglobals()); return $factory->createFromGlobals(); } From 0674874352e7ac5c437042b23362833383bc14ac Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 09:51:16 +0900 Subject: [PATCH 438/485] refactor: remove adjustPathTrailingSlash() --- system/HTTP/SiteURI.php | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 72196e1bc365..f4baa343ed37 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -384,7 +384,7 @@ public function baseUrl($relativePath = '', ?string $scheme = null): string return URI::createURIString( $uri->getScheme(), $uri->getAuthority(), - $this->adjustPathTrailingSlash($uri, $relativePath), + $uri->getPath(), $uri->getQuery(), $uri->getFragment() ); @@ -420,30 +420,12 @@ public function siteUrl($relativePath = '', ?string $scheme = null, ?App $config $uri = new self($config, $relativePath, $host, $scheme); - // Adjust path - $path = $this->adjustPathTrailingSlash($uri, $relativePath); - if ($config->indexPage !== '' && $relativePath === '') { - $path = rtrim($path, '/'); - } - return URI::createURIString( $uri->getScheme(), $uri->getAuthority(), - $path, + $uri->getPath(), $uri->getQuery(), $uri->getFragment() ); } - - private function adjustPathTrailingSlash(self $uri, string $relativePath): string - { - $parts = parse_url($this->getBaseURL() . $relativePath); - $path = $parts['path'] ?? ''; - - if (substr($path, -1) === '/' && substr($uri->getPath(), -1) !== '/') { - return $uri->getPath() . '/'; - } - - return $uri->getPath(); - } } From de46d3e746b50df17dde0e45b682569789a0c290 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 26 Feb 2023 16:47:55 +0900 Subject: [PATCH 439/485] fix: SiteURI's query is not set in FeatureTestTrait --- system/Test/FeatureTestTrait.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index ded49bd35011..5647430609f0 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -183,6 +183,8 @@ public function call(string $method, string $path, ?array $params = null) /** * Performs a GET request. * + * @param string $path URI path relative to baseURL. May include query. + * * @return TestResponse * * @throws RedirectException @@ -278,6 +280,7 @@ protected function setupRequest(string $method, ?string $path = null): IncomingR $_SERVER['QUERY_STRING'] = $query; $uri->setPath($path); + $uri->setQuery($query); Services::injectMock('uri', $uri); From 9827e0e068d87b917bce664ba27c3d9b3edd2f0f Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 26 Feb 2023 17:01:18 +0900 Subject: [PATCH 440/485] fix: use SiteURI in ControllerTestTrait --- system/Test/ControllerTestTrait.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/Test/ControllerTestTrait.php b/system/Test/ControllerTestTrait.php index 828dabb9e946..9ca47e24196c 100644 --- a/system/Test/ControllerTestTrait.php +++ b/system/Test/ControllerTestTrait.php @@ -100,7 +100,8 @@ protected function setUpControllerTestTrait(): void } if (! $this->uri instanceof URI) { - $this->uri = Services::uri($this->appConfig->baseURL ?? 'http://example.com/', false); + $factory = Services::siteurifactory($_SERVER, $this->appConfig, false); + $this->uri = $factory->createFromGlobals(); } if (empty($this->request)) { From 32078427cf6c5c74477e710157bcb60bf0c6f803 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 26 Feb 2023 17:03:50 +0900 Subject: [PATCH 441/485] test: update assertion URL (add `index.php/`) --- tests/system/CommonFunctionsTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 1ce49c4cd37e..c678a0dd1fbb 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -609,7 +609,10 @@ public function testForceHttpsNullRequestAndResponse(): void force_https(); } catch (Exception $e) { $this->assertInstanceOf(RedirectException::class, $e); - $this->assertSame('https://example.com/', $e->getResponse()->header('Location')->getValue()); + $this->assertSame( + 'https://example.com/index.php/', + $e->getResponse()->header('Location')->getValue() + ); $this->assertFalse($e->getResponse()->hasCookie('force')); $this->assertSame('header', $e->getResponse()->getHeaderLine('Force')); $this->assertSame('', $e->getResponse()->getBody()); From a4ba66d4f329f4b6965ce49e8a52d408dd7d80fc Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 17:38:45 +0900 Subject: [PATCH 442/485] refactor: remove unused private method --- system/HTTP/IncomingRequest.php | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 4f0f1cb0d85d..8734e8f97552 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -472,30 +472,6 @@ public function setPath(string $path, ?App $config = null) return $this; } - /** - * @deprecated 4.4.0 Moved to SiteURIFactory. - */ - private function determineHost(App $config, string $baseURL): string - { - $host = parse_url($baseURL, PHP_URL_HOST); - - if (empty($config->allowedHostnames)) { - return $host; - } - - // Update host if it is valid. - $httpHostPort = $this->getServer('HTTP_HOST'); - if ($httpHostPort !== null) { - [$httpHost] = explode(':', $httpHostPort, 2); - - if (in_array($httpHost, $config->allowedHostnames, true)) { - $host = $httpHost; - } - } - - return $host; - } - /** * Returns the URI path relative to baseURL, * running detection as necessary. From 9ec7fbc83ba7fbbfd228664176e88e54ae45c656 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 17:45:07 +0900 Subject: [PATCH 443/485] feat: add Services::superglobals() --- system/Config/BaseService.php | 1 + system/Config/Services.php | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 474e5e06cfa3..b66b8c9f535e 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -126,6 +126,7 @@ * @method static Security security(App $config = null, $getShared = true) * @method static Session session(App $config = null, $getShared = true) * @method static SiteURIFactory siteurifactory(App $config = null, Superglobals $superglobals = null, $getShared = true) + * @method static Superglobals superglobals(array $server = null, array $get = null, bool $getShared = true) * @method static Throttler throttler($getShared = true) * @method static Timer timer($getShared = true) * @method static Toolbar toolbar(ConfigToolbar $config = null, $getShared = true) diff --git a/system/Config/Services.php b/system/Config/Services.php index 6355b92ecc63..ca88b10221ca 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -710,11 +710,28 @@ public static function siteurifactory( } $config ??= config('App'); - $superglobals ??= new Superglobals(); + $superglobals ??= AppServices::superglobals(); return new SiteURIFactory($config, $superglobals); } + /** + * Superglobals. + * + * @return Superglobals + */ + public static function superglobals( + ?array $server = null, + ?array $get = null, + bool $getShared = true + ) { + if ($getShared) { + return static::getSharedInstance('superglobals', $server, $get); + } + + return new Superglobals($server, $get); + } + /** * The Throttler class provides a simple method for implementing * rate limiting in your applications. @@ -776,7 +793,7 @@ public static function uri(?string $uri = null, bool $getShared = true) if ($uri === null) { $appConfig = config(App::class); - $factory = AppServices::siteurifactory($appConfig, new Superglobals()); + $factory = AppServices::siteurifactory($appConfig, AppServices::superglobals()); return $factory->createFromGlobals(); } From f00cd8cfbd5fb89284a970538f96c09c799274e3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 17:54:46 +0900 Subject: [PATCH 444/485] fix: out-of-dated parameters --- system/Test/ControllerTestTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Test/ControllerTestTrait.php b/system/Test/ControllerTestTrait.php index 9ca47e24196c..2948bc5790c0 100644 --- a/system/Test/ControllerTestTrait.php +++ b/system/Test/ControllerTestTrait.php @@ -100,7 +100,7 @@ protected function setUpControllerTestTrait(): void } if (! $this->uri instanceof URI) { - $factory = Services::siteurifactory($_SERVER, $this->appConfig, false); + $factory = Services::siteurifactory($this->appConfig, Services::superglobals(), false); $this->uri = $factory->createFromGlobals(); } From 70179192966efee0198d645f30c4e1dff716f026 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 17:55:26 +0900 Subject: [PATCH 445/485] test: use Services::superglobals() --- tests/_support/Config/Services.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/_support/Config/Services.php b/tests/_support/Config/Services.php index 9a553d0116a0..e051ea8d6b24 100644 --- a/tests/_support/Config/Services.php +++ b/tests/_support/Config/Services.php @@ -13,7 +13,6 @@ use CodeIgniter\HTTP\SiteURIFactory; use CodeIgniter\HTTP\URI; -use CodeIgniter\Superglobals; use Config\App; use Config\Services as BaseServices; use RuntimeException; @@ -46,7 +45,7 @@ public static function uri(?string $uri = null, bool $getShared = true) if ($uri === null) { $appConfig = config(App::class); - $factory = new SiteURIFactory($appConfig, new Superglobals()); + $factory = new SiteURIFactory($appConfig, Services::superglobals()); return $factory->createFromGlobals(); } From 715bd00da9e053b38f8341aa826ed4bfc6fc35f3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 18:11:11 +0900 Subject: [PATCH 446/485] test: update out-of-dated parameters --- tests/system/Helpers/URLHelper/MiscUrlTest.php | 3 ++- tests/system/Helpers/URLHelper/SiteUrlTest.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/system/Helpers/URLHelper/MiscUrlTest.php b/tests/system/Helpers/URLHelper/MiscUrlTest.php index c26e9e7fbf9b..ecb59e1ef2b5 100644 --- a/tests/system/Helpers/URLHelper/MiscUrlTest.php +++ b/tests/system/Helpers/URLHelper/MiscUrlTest.php @@ -17,6 +17,7 @@ use CodeIgniter\HTTP\SiteURIFactory; use CodeIgniter\HTTP\UserAgent; use CodeIgniter\Router\Exceptions\RouterException; +use CodeIgniter\Superglobals; use CodeIgniter\Test\CIUnitTestCase; use Config\App; use InvalidArgumentException; @@ -69,7 +70,7 @@ public function testPreviousURLUsesSessionFirst(): void private function createRequest(string $uri): void { - $factory = new SiteURIFactory($_SERVER, $this->config); + $factory = new SiteURIFactory($this->config, new Superglobals()); $uri = $factory->createFromString($uri); diff --git a/tests/system/Helpers/URLHelper/SiteUrlTest.php b/tests/system/Helpers/URLHelper/SiteUrlTest.php index a28a588cbc5f..1a03d80f3521 100644 --- a/tests/system/Helpers/URLHelper/SiteUrlTest.php +++ b/tests/system/Helpers/URLHelper/SiteUrlTest.php @@ -17,6 +17,7 @@ use CodeIgniter\HTTP\SiteURIFactory; use CodeIgniter\HTTP\URI; use CodeIgniter\HTTP\UserAgent; +use CodeIgniter\Superglobals; use CodeIgniter\Test\CIUnitTestCase; use Config\App; @@ -55,7 +56,7 @@ private function createRequest(?App $config = null, $body = null, ?string $path { $config ??= new App(); - $factory = new SiteURIFactory($_SERVER, $config); + $factory = new SiteURIFactory($config, new Superglobals()); $uri = $factory->createFromGlobals(); if ($path !== null) { From f24ca6ac93d02f61dc930c2d18054f80fb41db92 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jul 2023 18:22:09 +0900 Subject: [PATCH 447/485] docs: add doc comments --- system/HTTP/SiteURIFactory.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index f0b846a44457..c6636f33898f 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -15,6 +15,11 @@ use CodeIgniter\Superglobals; use Config\App; +/** + * Creates SiteURI using superglobals. + * + * This class also updates superglobal $_SERVER and $_GET. + */ final class SiteURIFactory { private App $appConfig; @@ -42,6 +47,7 @@ public function createFromGlobals(): SiteURI * Create the SiteURI object from URI string. * * @internal Used for testing purposes only. + * @testTag */ public function createFromString(string $uri): SiteURI { @@ -79,6 +85,7 @@ public function createFromString(string $uri): SiteURI * @return string The route path * * @internal Used for testing purposes only. + * @testTag */ public function detectRoutePath(string $protocol = ''): string { From 7514d6d595272de1c186c7a820911947ed5ac632 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 28 Jul 2023 13:02:41 +0900 Subject: [PATCH 448/485] fix: support protocol-relative links --- system/HTTP/SiteURI.php | 28 +++++++++++++--------------- system/Helpers/url_helper.php | 1 - tests/system/HTTP/SiteURITest.php | 11 +++++++++++ 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index f4baa343ed37..287146e64c7c 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -145,7 +145,7 @@ private function determineBaseURL( $uri = new URI($baseURL); // Update scheme - if ($scheme !== null) { + if ($scheme !== null && $scheme !== '') { $uri->setScheme($scheme); } elseif ($configApp->forceGlobalSecureRequests) { $uri->setScheme('https'); @@ -381,13 +381,12 @@ public function baseUrl($relativePath = '', ?string $scheme = null): string $uri = new self($config, $relativePath, $host, $scheme); - return URI::createURIString( - $uri->getScheme(), - $uri->getAuthority(), - $uri->getPath(), - $uri->getQuery(), - $uri->getFragment() - ); + // Support protocol-relative links + if ($scheme === '') { + return substr((string) $uri, strlen($uri->getScheme()) + 1); + } + + return (string) $uri; } /** @@ -420,12 +419,11 @@ public function siteUrl($relativePath = '', ?string $scheme = null, ?App $config $uri = new self($config, $relativePath, $host, $scheme); - return URI::createURIString( - $uri->getScheme(), - $uri->getAuthority(), - $uri->getPath(), - $uri->getQuery(), - $uri->getFragment() - ); + // Support protocol-relative links + if ($scheme === '') { + return substr((string) $uri, strlen($uri->getScheme()) + 1); + } + + return (string) $uri; } } diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php index b8c1a687e6bb..a74fe944f148 100644 --- a/system/Helpers/url_helper.php +++ b/system/Helpers/url_helper.php @@ -33,7 +33,6 @@ function site_url($relativePath = '', ?string $scheme = null, ?App $config = nul assert($currentURI instanceof SiteURI); - // @TODO supprot protocol-relative links return $currentURI->siteUrl($relativePath, $scheme, $config); } } diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 392394086d30..f839b51c0771 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -293,6 +293,17 @@ public function testConstructorScheme() $this->assertSame('https://example.com/', $uri->getBaseURL()); } + public function testConstructorEmptyScheme() + { + $config = new App(); + + $uri = new SiteURI($config, '', null, ''); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://example.com/index.php', (string) $uri); + $this->assertSame('http://example.com/', $uri->getBaseURL()); + } + public function testConstructorForceGlobalSecureRequests() { $config = new App(); From ae4f7c84caeca41e93d1c9529d3215e5c1f63d94 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 28 Jul 2023 16:59:28 +0900 Subject: [PATCH 449/485] docs: add @deprecated versions --- system/HTTP/IncomingRequest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 8734e8f97552..947f5cc4c668 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -234,7 +234,7 @@ public function detectLocale($config) * * @return void * - * @deprecated No longer used. + * @deprecated 4.4.0 No longer used. */ protected function detectURI(string $protocol, string $baseURL) { @@ -463,7 +463,7 @@ public function isSecure(): bool * * @return $this * - * @deprecated This method will be private. The parameter $config is deprecated. No longer used. + * @deprecated 4.4.0 This method will be private. The parameter $config is deprecated. No longer used. */ public function setPath(string $path, ?App $config = null) { @@ -911,7 +911,7 @@ public function getFile(string $fileID) * * Do some final cleaning of the URI and return it, currently only used in static::_parse_request_uri() * - * @deprecated Use URI::removeDotSegments() directly + * @deprecated 4.1.2 Use URI::removeDotSegments() directly */ protected function removeRelativeDirectory(string $uri): string { From 6c83470cace439e0be8021547be560e6cca1aab7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 28 Jul 2023 16:56:09 +0900 Subject: [PATCH 450/485] docs: update user guide --- user_guide_src/source/changelogs/v4.4.0.rst | 42 +++++++++++++++++++ .../source/incoming/incomingrequest.rst | 24 +++++++---- .../source/incoming/incomingrequest/021.php | 3 +- .../source/incoming/incomingrequest/022.php | 15 ------- .../source/installation/upgrade_440.rst | 9 ++++ user_guide_src/source/libraries/uri.rst | 33 +++++++++++---- user_guide_src/source/libraries/uri/001.php | 2 +- user_guide_src/source/libraries/uri/002.php | 2 +- user_guide_src/source/libraries/uri/003.php | 1 - user_guide_src/source/libraries/uri/005.php | 2 +- 10 files changed, 96 insertions(+), 37 deletions(-) delete mode 100644 user_guide_src/source/incoming/incomingrequest/022.php diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 4b7b065cd6e2..d7ec516fa8da 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -72,6 +72,46 @@ CodeIgniter and exit() The ``CodeIgniter::run()`` method no longer calls ``exit(EXIT_SUCCESS)``. The exit call is moved to **public/index.php**. +.. _v440-site-uri-changes: + +Site URI Changes +================ + +A new ``SiteURI`` class that extends the ``URI`` class and represents the site +URI has been added, and now it is used in many places that need the current URI. + +``$this->request->getUri()`` in controllers returns the ``SiteURI`` instance. +Also, :php:func:`site_url()`, :php:func:`base_url()`, and :php:func:`current_url()` +use the SiteURI internally. + +getPath() +--------- + +The ``getPath()`` method now always returns the full URI path with leading ``/``. +Therefore, when your baseURL has sub-directories and you want to get the relative +path to baseURL, you must use the new ``getRoutePath()`` method instead. + +For example:: + + baseURL: http://localhost:8888/CodeIgniter4/ + The current URI: http://localhost:8888/CodeIgniter4/foo/bar + getPath(): /CodeIgniter4/foo/bar + getRoutePath(): foo/bar + +Site URI Values +--------------- + +The SiteURI class normalizes site URIs more strictly than before, and some bugs +have been fixed. + +As a result, the framework may return site URIs or the URI paths slightly differently +than in previous versions. +For example, ``/`` will be added after ``index.php``:: + + http://example.com/test/index.php?page=1 + ↓ + http://example.com/test/index.php/?page=1 + .. _v440-interface-changes: Interface Changes @@ -192,6 +232,8 @@ Libraries the value of the `full_path` index of the file if it was uploaded via directory upload. - **CURLRequest:** Added a request option ``proxy``. See :ref:`CURLRequest Class `. +- **URI:** A new ``SiteURI`` class that extends ``URI`` and represents the site + URI has been added. Helpers and Functions ===================== diff --git a/user_guide_src/source/incoming/incomingrequest.rst b/user_guide_src/source/incoming/incomingrequest.rst index ed3c40ec7f2b..5a58ea1c1608 100644 --- a/user_guide_src/source/incoming/incomingrequest.rst +++ b/user_guide_src/source/incoming/incomingrequest.rst @@ -228,11 +228,10 @@ The object gives you full abilities to grab any part of the request on it's own: .. literalinclude:: incomingrequest/021.php -You can work with the current URI string (the path relative to your baseURL) using the ``getPath()`` and ``setPath()`` methods. -Note that this relative path on the shared instance of ``IncomingRequest`` is what the :doc:`URL Helper ` -functions use, so this is a helpful way to "spoof" an incoming request for testing: +You can work with the current URI string (the path relative to your baseURL) using the ``getRoutePath()``. -.. literalinclude:: incomingrequest/022.php +.. note:: The ``getRoutePath()`` method can be used since v4.4.0. Prior to v4.4.0, + the ``getPath()`` method returned the path relative to your baseURL. Uploaded Files ************** @@ -476,15 +475,22 @@ The methods provided by the parent classes that are available are: :returns: The current URI path relative to baseURL :rtype: string - This is the safest method to determine the "current URI", since ``IncomingRequest::$uri`` - may not be aware of the complete App configuration for base URLs. + This method returns the current URI path relative to baseURL. + + .. note:: Prior to v4.4.0, this was the safest method to determine the + "current URI", since ``IncomingRequest::$uri`` might not be aware of + the complete App configuration for base URLs. .. php:method:: setPath($path) + .. deprecated:: 4.4.0 + :param string $path: The relative path to use as the current URI :returns: This Incoming Request :rtype: IncomingRequest - Used mostly just for testing purposes, this allows you to set the relative path - value for the current request instead of relying on URI detection. This will also - update the underlying ``URI`` instance with the new path. + .. note:: Prior to v4.4.0, used mostly just for testing purposes, this + allowed you to set the relative path value for the current request + instead of relying on URI detection. This also updated the + underlying ``URI`` instance with the new path. + diff --git a/user_guide_src/source/incoming/incomingrequest/021.php b/user_guide_src/source/incoming/incomingrequest/021.php index 558bb6a98e6f..9a1b8601b07c 100644 --- a/user_guide_src/source/incoming/incomingrequest/021.php +++ b/user_guide_src/source/incoming/incomingrequest/021.php @@ -7,7 +7,8 @@ echo $uri->getUserInfo(); // snoopy:password echo $uri->getHost(); // example.com echo $uri->getPort(); // 88 -echo $uri->getPath(); // path/to/page +echo $uri->getPath(); // /path/to/page +echo $uri->getRoutePath(); // path/to/page echo $uri->getQuery(); // foo=bar&bar=baz print_r($uri->getSegments()); // Array ( [0] => path [1] => to [2] => page ) echo $uri->getSegment(1); // path diff --git a/user_guide_src/source/incoming/incomingrequest/022.php b/user_guide_src/source/incoming/incomingrequest/022.php deleted file mode 100644 index 723d81e3a3a3..000000000000 --- a/user_guide_src/source/incoming/incomingrequest/022.php +++ /dev/null @@ -1,15 +0,0 @@ -setPath('users/list'); - - $menu = new MyMenu(); - - $this->assertTrue('users/list', $menu->getActiveLink()); - } -} diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index ffb8751c5206..52a4d45066d9 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -47,6 +47,15 @@ If your code depends on this bug, fix the segment number. .. literalinclude:: upgrade_440/002.php :lines: 2- +Site URI Changes +================ + +When your baseURL has sub-directories and you get the relative path to baseURL of +the current URI by the ``URI::getPath()`` method, you must use the new +``SiteURI::getRoutePath()`` method instead. + +See :ref:`v440-site-uri-changes` for details. + When You Extend Exceptions ========================== diff --git a/user_guide_src/source/libraries/uri.rst b/user_guide_src/source/libraries/uri.rst index bfd189859bf4..3ce031bf64bf 100644 --- a/user_guide_src/source/libraries/uri.rst +++ b/user_guide_src/source/libraries/uri.rst @@ -14,34 +14,47 @@ relative URI to an existing one and have it resolved safely and correctly. Creating URI instances ====================== -Creating a URI instance is as simple as creating a new class instance: +Creating a URI instance is as simple as creating a new class instance. + +When you create the new instance, you can pass a full or partial URL in the constructor and it will be parsed +into its appropriate sections: .. literalinclude:: uri/001.php + :lines: 2- -Alternatively, you can use the ``service()`` function to return an instance for you: +Alternatively, you can use the :php:func:`service()` function to return an instance for you: -.. literalinclude:: uri/002.php +.. literalinclude:: uri/003.php + :lines: 2- -When you create the new instance, you can pass a full or partial URL in the constructor and it will be parsed -into its appropriate sections: +Since v4.4.0, if you don't pass a URL, it returns the current URI: -.. literalinclude:: uri/003.php +.. literalinclude:: uri/002.php + :lines: 2- + +.. note:: The above code returns the ``SiteURI`` instance, that extends the ``URI`` + class. The ``URI`` class is for general URIs, but the ``SiteURI`` class is + for your site URIs. The Current URI --------------- Many times, all you really want is an object representing the current URL of this request. -You can use one of the functions available in the :doc:`../helpers/url_helper`: +You can use the :php:func:`current_url()` function available in the :doc:`../helpers/url_helper`: .. literalinclude:: uri/004.php + :lines: 2- You must pass ``true`` as the first parameter, otherwise, it will return the string representation of the current URL. This URI is based on the path (relative to your ``baseURL``) as determined by the current request object and your settings in ``Config\App`` (``baseURL``, ``indexPage``, and ``forceGlobalSecureRequests``). -Assuming that you're in a controller that extends ``CodeIgniter\Controller`` you can get this relative path: + +Assuming that you're in a controller that extends ``CodeIgniter\Controller``, you +can also get the current SiteURI instance: .. literalinclude:: uri/005.php + :lines: 2- =========== URI Strings @@ -136,6 +149,10 @@ can be used to manipulate it: .. note:: When setting the path this way, or any other way the class allows, it is sanitized to encode any dangerous characters, and remove dot segments for safety. +.. note:: Since v4.4.0, the ``SiteURI::getRoutePath()`` method, + returns the URI path relative to baseURL, and the ``SiteURI::getPath()`` + method always returns the full URI path with leading ``/``. + Query ----- diff --git a/user_guide_src/source/libraries/uri/001.php b/user_guide_src/source/libraries/uri/001.php index 524e67669110..7219d03d248f 100644 --- a/user_guide_src/source/libraries/uri/001.php +++ b/user_guide_src/source/libraries/uri/001.php @@ -1,3 +1,3 @@ request->getPath(); +$uri = $this->request->getUri(); From e4a2523d4c2868fe83592a842388d1444eb59d98 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 28 Jul 2023 17:17:33 +0900 Subject: [PATCH 451/485] docs: add @deprecated versions --- system/HTTP/URI.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 429aa73a7b7f..fb9d780c1485 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -36,14 +36,14 @@ class URI * * @var string * - * @deprecated Not used. + * @deprecated 4.4.0 Not used. */ protected $uriString; /** * The Current baseURL. * - * @deprecated Use SiteURI instead. + * @deprecated 4.4.0 Use SiteURI instead. */ private ?string $baseURL = null; @@ -292,7 +292,7 @@ public function useRawQueryString(bool $raw = true) * * @throws HTTPException * - * @deprecated This method will be private. + * @deprecated 4.4.0 This method will be private. */ public function setURI(?string $uri = null) { @@ -705,7 +705,7 @@ public function setAuthority(string $str) * * @return $this * - * @deprecated Use `withScheme()` instead. + * @deprecated 4.4.0 Use `withScheme()` instead. */ public function setScheme(string $str) { From c2f88600ee1074aa979690566b8d1031277a5fbe Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 28 Jul 2023 17:17:46 +0900 Subject: [PATCH 452/485] docs: add Deprecations --- user_guide_src/source/changelogs/v4.4.0.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index d7ec516fa8da..9ce187ddae98 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -324,8 +324,20 @@ Deprecations ``$redirect`` in ``Security`` are deprecated, and no longer used. Use ``$config`` instead. - **URI:** + - ``URI::$uriString`` is deprecated. + - ``URI::$baseURL`` is deprecated. Use ``SiteURI`` instead. - ``URI::setSilent()`` is deprecated. - ``URI::setScheme()`` is deprecated. Use ``withScheme()`` instead. + - ``URI::setURI()`` is deprecated. +- **IncomingRequest:** + - ``IncomingRequest::detectURI()`` is deprecated and no longer used. + - ``IncomingRequest::detectPath()`` is deprecated, and no longer used. It + moved to ``SiteURIFactory``. + - ``IncomingRequest::parseRequestURI()`` is deprecated, and no longer used. It + moved to ``SiteURIFactory``. + - ``IncomingRequest::parseQueryString()`` is deprecated, and no longer used. It + moved to ``SiteURIFactory``. + - ``IncomingRequest::setPath()`` is deprecated. Bugs Fixed ********** From e01d6220874d9fa95e56eb517acd0024adfa4fc0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 31 Jul 2023 17:29:37 +0900 Subject: [PATCH 453/485] test: remove unused private method --- tests/system/Helpers/URLHelper/CurrentUrlTest.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/system/Helpers/URLHelper/CurrentUrlTest.php b/tests/system/Helpers/URLHelper/CurrentUrlTest.php index a28fa78b9b98..c735064fa287 100644 --- a/tests/system/Helpers/URLHelper/CurrentUrlTest.php +++ b/tests/system/Helpers/URLHelper/CurrentUrlTest.php @@ -194,15 +194,6 @@ public function testUriString(): void $this->assertSame('assets/image.jpg', uri_string()); } - private function setService(string $uri): void - { - $uri = new URI($uri); - Services::injectMock('uri', $uri); - - $request = Services::request($this->config); - Services::injectMock('request', $request); - } - public function testUriStringNoTrailingSlash(): void { $_SERVER['HTTP_HOST'] = 'example.com'; From eeca9e9a023d18dc502082097b2c060e5afd8b82 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 4 Aug 2023 13:47:18 +0900 Subject: [PATCH 454/485] docs: fix section level --- user_guide_src/source/changelogs/v4.4.0.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 9ce187ddae98..c67497ede1a2 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -75,7 +75,7 @@ exit call is moved to **public/index.php**. .. _v440-site-uri-changes: Site URI Changes -================ +---------------- A new ``SiteURI`` class that extends the ``URI`` class and represents the site URI has been added, and now it is used in many places that need the current URI. @@ -85,7 +85,7 @@ Also, :php:func:`site_url()`, :php:func:`base_url()`, and :php:func:`current_url use the SiteURI internally. getPath() ---------- +^^^^^^^^^ The ``getPath()`` method now always returns the full URI path with leading ``/``. Therefore, when your baseURL has sub-directories and you want to get the relative @@ -99,7 +99,7 @@ For example:: getRoutePath(): foo/bar Site URI Values ---------------- +^^^^^^^^^^^^^^^ The SiteURI class normalizes site URIs more strictly than before, and some bugs have been fixed. From 975281545114848324aecd6bdc3028ef8829abb0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 4 Aug 2023 14:09:45 +0900 Subject: [PATCH 455/485] test: update data provider method name --- tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index 1e96bf065b92..2890085627f3 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -278,7 +278,7 @@ public function testPathInfoSubfolder() } /** - * @dataProvider providePathChecks + * @dataProvider provideExtensionPHP * * @param string $path * @param string $detectPath @@ -296,7 +296,7 @@ public function testExtensionPHP($path, $detectPath) $this->assertSame($detectPath, $factory->detectRoutePath()); } - public function providePathChecks(): iterable + public function provideExtensionPHP(): iterable { return [ 'not /index.php' => [ From d80c7b5a35742d69fb70e74c88b2803b8db6c4b1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 9 Aug 2023 14:31:18 +0900 Subject: [PATCH 456/485] fix: ControllerTestTrait::withUri() updates Request instance --- system/Test/ControllerTestTrait.php | 8 +++++++- user_guide_src/source/changelogs/v4.4.0.rst | 4 ++++ user_guide_src/source/testing/controllers.rst | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/system/Test/ControllerTestTrait.php b/system/Test/ControllerTestTrait.php index 2948bc5790c0..7e4091be6175 100644 --- a/system/Test/ControllerTestTrait.php +++ b/system/Test/ControllerTestTrait.php @@ -278,7 +278,13 @@ public function withLogger($logger) */ public function withUri(string $uri) { - $this->uri = new URI($uri); + $factory = Services::siteurifactory(); + $this->uri = $factory->createFromString($uri); + Services::injectMock('uri', $this->uri); + + // Update the Request instance, because Request has the SiteURI instance. + $this->request = Services::incomingrequest(null, false); + Services::injectMock('request', $this->request); return $this; } diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index c67497ede1a2..a6a3bdde7d88 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -347,6 +347,10 @@ Bugs Fixed (e.g., **foo-bar**) and one URI for underscores (e.g., **foo_bar**). This bug has been fixed. Now the URI for underscores (**foo_bar**) is not accessible. - **Output Buffering:** Bug fix with output buffering. +- **ControllerTestTrait:** ``ControllerTestTrait::withUri()`` creates a new Request + instance with the URI. Because the Request instance should have the URI instance. + Also if the hostname in the URI string is invalid with ``Config\App``, the valid + hostname will be set. See the repo's `CHANGELOG.md `_ diff --git a/user_guide_src/source/testing/controllers.rst b/user_guide_src/source/testing/controllers.rst index 130364260cba..8f38f68c4e3d 100644 --- a/user_guide_src/source/testing/controllers.rst +++ b/user_guide_src/source/testing/controllers.rst @@ -101,6 +101,11 @@ representing a valid URI: It is a good practice to always provide the URI during testing to avoid surprises. +.. note:: Since v4.4.0, this method creates a new Request instance with the URI. + Because the Request instance should have the URI instance. Also if the hostname + in the URI string is invalid with ``Config\App``, the valid hostname will be + set. + withBody($body) --------------- From 3a76189864395859a819e29442b116138918bd26 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 9 Aug 2023 14:52:41 +0900 Subject: [PATCH 457/485] test: update data provider method name --- tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index 2890085627f3..34b242b57ffa 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -296,7 +296,7 @@ public function testExtensionPHP($path, $detectPath) $this->assertSame($detectPath, $factory->detectRoutePath()); } - public function provideExtensionPHP(): iterable + public static function provideExtensionPHP(): iterable { return [ 'not /index.php' => [ From 0eebc5e60b545ae062fc094f721608dcbf92af85 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 9 Aug 2023 16:16:09 +0900 Subject: [PATCH 458/485] test: add tests for SiteURIFactory --- tests/system/HTTP/SiteURIFactoryTest.php | 89 ++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 7 deletions(-) diff --git a/tests/system/HTTP/SiteURIFactoryTest.php b/tests/system/HTTP/SiteURIFactoryTest.php index 138ccad0b48d..c7522c8cf089 100644 --- a/tests/system/HTTP/SiteURIFactoryTest.php +++ b/tests/system/HTTP/SiteURIFactoryTest.php @@ -85,16 +85,91 @@ public function testCreateFromGlobalsAllowedHost() $this->assertSame('woot', $uri->getRoutePath()); } - public function testCreateFromString() - { + /** + * @dataProvider provideCreateFromStringWithIndexPage + */ + public function testCreateFromStringWithIndexPage( + string $uriString, + string $expectUriString, + string $expectedPath, + string $expectedRoutePath + ) { $factory = $this->createSiteURIFactory(); - $uriString = 'http://invalid.example.jp/foo/bar?page=3'; - $uri = $factory->createFromString($uriString); + $uri = $factory->createFromString($uriString); $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('http://localhost:8080/index.php/foo/bar?page=3', (string) $uri); - $this->assertSame('/index.php/foo/bar', $uri->getPath()); - $this->assertSame('foo/bar', $uri->getRoutePath()); + $this->assertSame($expectUriString, (string) $uri); + $this->assertSame($expectedPath, $uri->getPath()); + $this->assertSame($expectedRoutePath, $uri->getRoutePath()); + } + + public static function provideCreateFromStringWithIndexPage(): iterable + { + return [ + 'indexPage path query' => [ + 'http://invalid.example.jp/foo/bar?page=3', // $uriString + 'http://localhost:8080/index.php/foo/bar?page=3', // $expectUriString + '/index.php/foo/bar', // $expectedPath + 'foo/bar', // $expectedRoutePath + ], + 'indexPage noPath' => [ + 'http://localhost:8080', // $uriString + 'http://localhost:8080/index.php', // $expectUriString + '/index.php', // $expectedPath + '', // $expectedRoutePath + ], + 'indexPage slash' => [ + 'http://localhost:8080/', // $uriString + 'http://localhost:8080/index.php/', // $expectUriString + '/index.php/', // $expectedPath + '', // $expectedRoutePath + ], + ]; + } + + /** + * @dataProvider provideCreateFromStringWithoutIndexPage + */ + public function testCreateFromStringWithoutIndexPage( + string $uriString, + string $expectUriString, + string $expectedPath, + string $expectedRoutePath + ) { + $config = new App(); + $config->indexPage = ''; + $factory = $this->createSiteURIFactory($config); + + $uri = $factory->createFromString($uriString); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame($expectUriString, (string) $uri); + $this->assertSame($expectedPath, $uri->getPath()); + $this->assertSame($expectedRoutePath, $uri->getRoutePath()); + } + + public static function provideCreateFromStringWithoutIndexPage(): iterable + { + return [ + 'path query' => [ + 'http://invalid.example.jp/foo/bar?page=3', // $uriString + 'http://localhost:8080/foo/bar?page=3', // $expectUriString + '/foo/bar', // $expectedPath + 'foo/bar', // $expectedRoutePath + ], + 'noPath' => [ + 'http://localhost:8080', // $uriString + 'http://localhost:8080/', // $expectUriString + '/', // $expectedPath + '', // $expectedRoutePath + ], + 'slash' => [ + 'http://localhost:8080/', // $uriString + 'http://localhost:8080/', // $expectUriString + '/', // $expectedPath + '', // $expectedRoutePath + ], + ]; } } From d9a5a6d1feadc9c7b89b9bd979710100535ce92a Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 9 Aug 2023 16:16:42 +0900 Subject: [PATCH 459/485] fix: when URI string does not have path --- system/HTTP/SiteURIFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index c6636f33898f..e250c559058c 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -70,7 +70,7 @@ public function createFromString(string $uri): SiteURI $fragment = '#' . $parts['fragment']; } - $relativePath = $parts['path'] . $query . $fragment; + $relativePath = ($parts['path'] ?? '') . $query . $fragment; $host = $this->getValidHost($parts['host']); return new SiteURI($this->appConfig, $relativePath, $host, $parts['scheme']); From 546d11200296d22f0690109fee5b40148480fc70 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 10 Aug 2023 09:02:58 +0900 Subject: [PATCH 460/485] test: remove redundant test --- tests/system/Test/FeatureTestTraitTest.php | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/tests/system/Test/FeatureTestTraitTest.php b/tests/system/Test/FeatureTestTraitTest.php index 75d0db1a1d57..d4e5195b3347 100644 --- a/tests/system/Test/FeatureTestTraitTest.php +++ b/tests/system/Test/FeatureTestTraitTest.php @@ -52,26 +52,13 @@ public function testCallGet(): void ]); $response = $this->get('home'); - $response->assertSee('Hello World'); - $response->assertDontSee('Again'); - } - - public function testCallSimpleGet(): void - { - $this->withRoutes([ - [ - 'add', - 'home', - static fn () => 'Hello Earth', - ], - ]); - $response = $this->call('get', 'home'); - $this->assertInstanceOf(TestResponse::class, $response); $this->assertInstanceOf(Response::class, $response->response()); $this->assertTrue($response->isOK()); - $this->assertSame('Hello Earth', $response->response()->getBody()); + $this->assertSame('Hello World', $response->response()->getBody()); $this->assertSame(200, $response->response()->getStatusCode()); + $response->assertSee('Hello World'); + $response->assertDontSee('Again'); } public function testClosureWithEcho() From 16621bf69d67fa1ceff1185791b4231ce5ca6abe Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 10 Aug 2023 08:56:06 +0900 Subject: [PATCH 461/485] test: add tests for uri_string() and current_url() --- tests/system/Test/ControllerTestTraitTest.php | 12 ++++++++++++ tests/system/Test/FeatureTestTraitTest.php | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/tests/system/Test/ControllerTestTraitTest.php b/tests/system/Test/ControllerTestTraitTest.php index b5d08e72ed8f..ab45c6372d41 100644 --- a/tests/system/Test/ControllerTestTraitTest.php +++ b/tests/system/Test/ControllerTestTraitTest.php @@ -19,6 +19,7 @@ use Config\App; use Config\Services; use Exception; +use Tests\Support\Controllers\Newautorouting; use Tests\Support\Controllers\Popcorn; /** @@ -259,4 +260,15 @@ public function throwsBody(): void $this->withBody('banana')->execute('throwsBody'); } + + public function testWithUriUpdatesUriStringAndCurrentUrlValues() + { + $result = $this->withURI('http://example.com/foo/bar/1/2/3') + ->controller(Newautorouting::class) + ->execute('postSave', '1', '2', '3'); + + $this->assertSame('Saved', $result->response()->getBody()); + $this->assertSame('foo/bar/1/2/3', uri_string()); + $this->assertSame('http://example.com/index.php/foo/bar/1/2/3', current_url()); + } } diff --git a/tests/system/Test/FeatureTestTraitTest.php b/tests/system/Test/FeatureTestTraitTest.php index d4e5195b3347..0f5bb7ce33d5 100644 --- a/tests/system/Test/FeatureTestTraitTest.php +++ b/tests/system/Test/FeatureTestTraitTest.php @@ -61,6 +61,22 @@ public function testCallGet(): void $response->assertDontSee('Again'); } + public function testCallGetAndUriString(): void + { + $this->withRoutes([ + [ + 'get', + 'foo/bar/1/2/3', + static fn () => 'Hello World', + ], + ]); + $response = $this->get('foo/bar/1/2/3'); + + $this->assertSame('Hello World', $response->response()->getBody()); + $this->assertSame('foo/bar/1/2/3', uri_string()); + $this->assertSame('http://example.com/index.php/foo/bar/1/2/3', current_url()); + } + public function testClosureWithEcho() { $this->withRoutes([ From cce185c92f4c34be520c9d4515350db61201acc0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 10 Aug 2023 10:22:08 +0900 Subject: [PATCH 462/485] docs: add upgrade_440 --- user_guide_src/source/installation/upgrade_440.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index 52a4d45066d9..c4bdfe0c81ff 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -50,9 +50,12 @@ If your code depends on this bug, fix the segment number. Site URI Changes ================ -When your baseURL has sub-directories and you get the relative path to baseURL of -the current URI by the ``URI::getPath()`` method, you must use the new -``SiteURI::getRoutePath()`` method instead. +- Because of the rework for the current URI determination, the framework may return + site URIs or the URI paths slightly differently than in previous versions. It may + break your test code. Update assertions if the existing tests fail. +- When your baseURL has sub-directories and you get the relative path to baseURL of + the current URI by the ``URI::getPath()`` method, you must use the new + ``SiteURI::getRoutePath()`` method instead. See :ref:`v440-site-uri-changes` for details. From f35dc9bc7bddffa79f3a44f94c99b604d77c146e Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 14 Aug 2023 18:11:59 +0900 Subject: [PATCH 463/485] docs: fix typo Co-authored-by: MGatner --- system/HTTP/SiteURI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 287146e64c7c..362c8ee32362 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -365,7 +365,7 @@ protected function applyParts(array $parts): void } /** - * For baser_url() helper. + * For base_url() helper. * * @param array|string $relativePath URI string or array of URI segments * @param string|null $scheme URI scheme. E.g., http, ftp From c6f3d57fdd51b059e810f854d8c649953dfddc4c Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 15 Aug 2023 10:19:19 +0900 Subject: [PATCH 464/485] refactor: use Services::superglobals() --- system/Test/FeatureTestTrait.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index 5647430609f0..b117a1d890ae 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -277,7 +277,8 @@ protected function setupRequest(string $method, ?string $path = null): IncomingR $path = $parts[0]; $query = $parts[1] ?? ''; - $_SERVER['QUERY_STRING'] = $query; + $superglobals = Services::superglobals(); + $superglobals->setServer('QUERY_STRING', $query); $uri->setPath($path); $uri->setQuery($query); From 92ecc14c4c158540a9acf6fc268c7b27d98debc7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Jul 2023 11:05:11 +0900 Subject: [PATCH 465/485] feat: add methods for caching --- system/Config/Factories.php | 58 ++++++++++++++++++++++++++- tests/system/Config/FactoriesTest.php | 55 +++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 5bf7afebd431..943c9055c982 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -80,6 +80,15 @@ class Factories */ protected static $instances = []; + /** + * Whether the component instances are updated? + * + * @var array [component => true] + * + * @internal For caching only + */ + protected static $updated = []; + /** * Define the class to load. You can *override* the concrete class. * @@ -153,6 +162,7 @@ public static function __callStatic(string $component, array $arguments) self::$instances[$options['component']][$class] = new $class(...$arguments); self::$aliases[$options['component']][$alias] = $class; + self::$updated[$options['component']] = true; // If a short classname is specified, also register FQCN to share the instance. if (! isset(self::$aliases[$options['component']][$class])) { @@ -383,7 +393,8 @@ public static function reset(?string $component = null) unset( static::$options[$component], static::$aliases[$component], - static::$instances[$component] + static::$instances[$component], + static::$updated[$component] ); return; @@ -392,6 +403,7 @@ public static function reset(?string $component = null) static::$options = []; static::$aliases = []; static::$instances = []; + static::$updated = []; } /** @@ -440,4 +452,48 @@ public static function getBasename(string $alias): string return $alias; } + + /** + * Gets component data for caching. + * + * @internal For caching only + */ + public static function getComponentInstances(string $component): array + { + if (! isset(static::$aliases[$component])) { + return [ + 'aliases' => [], + 'instances' => [], + ]; + } + + $data = [ + 'aliases' => static::$aliases[$component], + 'instances' => self::$instances[$component], + ]; + + return $data; + } + + /** + * Sets component data + * + * @internal For caching only + */ + public static function setComponentInstances(string $component, array $data) + { + static::$aliases[$component] = $data['aliases']; + self::$instances[$component] = $data['instances']; + unset(self::$updated[$component]); + } + + /** + * Whether the component instances are updated? + * + * @internal For caching only + */ + public static function isUpdated(string $component): bool + { + return isset(self::$updated[$component]) ? true : false; + } } diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index 3750113bbf73..d48a6e50141d 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -398,4 +398,59 @@ public function testDefineAndLoad() $this->assertInstanceOf(EntityModel::class, $model); } + + public function testGetComponentInstances() + { + Factories::config('App'); + Factories::config(\Config\Database::class); + + $data = Factories::getComponentInstances('config'); + + $this->assertIsArray($data); + $this->assertArrayHasKey('aliases', $data); + $this->assertArrayHasKey('instances', $data); + + return $data; + } + + /** + * @depends testGetComponentInstances + */ + public function testSetComponentInstances(array $data) + { + $before = Factories::getComponentInstances('config'); + $this->assertSame(['aliases' => [], 'instances' => []], $before); + + Factories::setComponentInstances('config', $data); + + $data = Factories::getComponentInstances('config'); + + $this->assertIsArray($data); + $this->assertArrayHasKey('aliases', $data); + $this->assertArrayHasKey('instances', $data); + + return $data; + } + + /** + * @depends testSetComponentInstances + */ + public function testIsUpdated(array $data) + { + Factories::reset(); + + $updated = $this->getFactoriesStaticProperty('updated'); + + $this->assertSame([], $updated); + $this->assertFalse(Factories::isUpdated('config')); + + Factories::config('App'); + + $this->assertTrue(Factories::isUpdated('config')); + $this->assertFalse(Factories::isUpdated('models')); + + Factories::setComponentInstances('config', $data); + + $this->assertFalse(Factories::isUpdated('config')); + } } From ccc532d871aee045947f8d85d3b0b73d9e7d14a6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Jul 2023 11:37:11 +0900 Subject: [PATCH 466/485] feat: add FactoriesCache for Config caching --- system/Cache/FactoriesCache.php | 65 ++++++++++++++ .../FactoriesCache/FileVarExportHandler.php | 46 ++++++++++ .../Cache/FactoriesCacheFileHandlerTest.php | 33 +++++++ ...FactoriesCacheFileVarExportHandlerTest.php | 90 +++++++++++++++++++ 4 files changed, 234 insertions(+) create mode 100644 system/Cache/FactoriesCache.php create mode 100644 system/Cache/FactoriesCache/FileVarExportHandler.php create mode 100644 tests/system/Cache/FactoriesCacheFileHandlerTest.php create mode 100644 tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php diff --git a/system/Cache/FactoriesCache.php b/system/Cache/FactoriesCache.php new file mode 100644 index 000000000000..7644ee5afbde --- /dev/null +++ b/system/Cache/FactoriesCache.php @@ -0,0 +1,65 @@ + + * + * 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\FactoriesCache\FileVarExportHandler; +use CodeIgniter\Config\Factories; + +final class FactoriesCache +{ + /** + * @var CacheInterface|FileVarExportHandler + */ + private $cache; + + /** + * @param CacheInterface|FileVarExportHandler $cache + */ + public function __construct($cache = null) + { + $this->cache = $cache ?? new FileVarExportHandler(); + } + + public function save(string $component): void + { + if (! Factories::isUpdated($component)) { + return; + } + + $data = Factories::getComponentInstances($component); + + $this->cache->save($this->getCacheKey($component), $data, 3600 * 24); + } + + private function getCacheKey(string $component) + { + return 'FactoriesCache_' . $component; + } + + public function load(string $component): bool + { + $key = $this->getCacheKey($component); + + if (! $data = $this->cache->get($key)) { + return false; + } + + Factories::setComponentInstances($component, $data); + + return true; + } + + public function delete(string $component): void + { + $this->cache->delete($this->getCacheKey($component)); + } +} diff --git a/system/Cache/FactoriesCache/FileVarExportHandler.php b/system/Cache/FactoriesCache/FileVarExportHandler.php new file mode 100644 index 000000000000..99a71d8bd23f --- /dev/null +++ b/system/Cache/FactoriesCache/FileVarExportHandler.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\FactoriesCache; + +final class FileVarExportHandler +{ + private string $path = WRITEPATH . 'cache'; + + /** + * @param array|bool|float|int|object|string|null $val + */ + public function save(string $key, $val): void + { + $val = var_export($val, true); + + // Write to temp file first to ensure atomicity + $tmp = $this->path . "/{$key}." . uniqid('', true) . '.tmp'; + file_put_contents($tmp, 'path . "/{$key}"); + } + + public function delete(string $key): void + { + @unlink($this->path . "/{$key}"); + } + + /** + * @return array|bool|float|int|object|string|null + */ + public function get(string $key) + { + @include $this->path . "/{$key}"; + + return $val ?? false; // @phpstan-ignore-line + } +} diff --git a/tests/system/Cache/FactoriesCacheFileHandlerTest.php b/tests/system/Cache/FactoriesCacheFileHandlerTest.php new file mode 100644 index 000000000000..b5434bbb1b1b --- /dev/null +++ b/tests/system/Cache/FactoriesCacheFileHandlerTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Cache; + +use Config\Cache as CacheConfig; + +/** + * @internal + * + * @group Others + */ +final class FactoriesCacheFileHandlerTest extends FactoriesCacheFileVarExportHandlerTest +{ + /** + * @var @var FileVarExportHandler|CacheInterface + */ + protected $handler; + + protected function createFactoriesCache(): void + { + $this->handler = CacheFactory::getHandler(new CacheConfig(), 'file'); + $this->cache = new FactoriesCache($this->handler); + } +} diff --git a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php new file mode 100644 index 000000000000..3fcf751b6176 --- /dev/null +++ b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php @@ -0,0 +1,90 @@ + + * + * 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\FactoriesCache\FileVarExportHandler; +use CodeIgniter\Config\Factories; +use CodeIgniter\Test\CIUnitTestCase; +use Config\App; + +/** + * @internal + * @no-final + * + * @group Others + */ +class FactoriesCacheFileVarExportHandlerTest extends CIUnitTestCase +{ + protected FactoriesCache $cache; + + /** + * @var CacheInterface|FileVarExportHandler + */ + protected $handler; + + protected function createFactoriesCache(): void + { + $this->handler = new FileVarExportHandler(); + $this->cache = new FactoriesCache($this->handler); + } + + public function testInstantiate() + { + $this->createFactoriesCache(); + + $this->assertInstanceOf(FactoriesCache::class, $this->cache); + } + + public function testSave() + { + Factories::reset(); + Factories::config('App'); + + $this->createFactoriesCache(); + + $this->cache->save('config'); + + $cachedData = $this->handler->get('FactoriesCache_config'); + + $this->assertArrayHasKey('basenames', $cachedData); + $this->assertArrayHasKey('instances', $cachedData); + $this->assertArrayHasKey('Modules', $cachedData['basenames']); + $this->assertArrayHasKey('App', $cachedData['basenames']); + } + + public function testLoad() + { + Factories::reset(); + /** @var App $appConfig */ + $appConfig = Factories::config('App'); + $appConfig->baseURL = 'http://test.example.jp/this-is-test/'; + + $this->createFactoriesCache(); + $this->cache->save('config'); + + Factories::reset(); + + $this->cache->load('config'); + + $appConfig = Factories::config('App'); + $this->assertSame('http://test.example.jp/this-is-test/', $appConfig->baseURL); + } + + public function testDelete() + { + $this->createFactoriesCache(); + + $this->cache->delete('config'); + + $this->assertFalse($this->cache->load('config')); + } +} From f9b95e3fa92dd982a84c0ee8bb8d5e41d1825a1d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Jul 2023 17:56:55 +0900 Subject: [PATCH 467/485] feat: add __set_state() for Config caching --- app/Config/Paths.php | 13 +++++++++++++ system/Config/BaseConfig.php | 17 +++++++++++++++++ system/Modules/Modules.php | 20 ++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/app/Config/Paths.php b/app/Config/Paths.php index 262c745ffa95..35a941940b5e 100644 --- a/app/Config/Paths.php +++ b/app/Config/Paths.php @@ -77,4 +77,17 @@ class Paths * is used when no value is provided to `Services::renderer()`. */ public string $viewDirectory = __DIR__ . '/../Views'; + + public static function __set_state(array $array) + { + $obj = new self(); + + $properties = array_keys(get_object_vars($obj)); + + foreach ($properties as $property) { + $obj->{$property} = $array[$property]; + } + + return $obj; + } } diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 0958d7ae7f75..fad688ab6214 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -26,6 +26,8 @@ * from the environment. * * These can be set within the .env file. + * + * @phpstan-consistent-constructor */ class BaseConfig { @@ -51,6 +53,21 @@ class BaseConfig */ protected static $moduleConfig; + public static function __set_state(array $array) + { + static::$override = false; + $obj = new static(); + static::$override = true; + + $properties = array_keys(get_object_vars($obj)); + + foreach ($properties as $property) { + $obj->{$property} = $array[$property]; + } + + return $obj; + } + /** * Will attempt to get environment variables with names * that match the properties of the child class. diff --git a/system/Modules/Modules.php b/system/Modules/Modules.php index ade3e693ceea..f99e7215e733 100644 --- a/system/Modules/Modules.php +++ b/system/Modules/Modules.php @@ -15,6 +15,8 @@ * Modules Class * * @see https://codeigniter.com/user_guide/general/modules.html + * + * @phpstan-consistent-constructor */ class Modules { @@ -39,6 +41,11 @@ class Modules */ public $aliases = []; + public function __construct() + { + // For @phpstan-consistent-constructor + } + /** * Should the application auto-discover the requested resource. */ @@ -50,4 +57,17 @@ public function shouldDiscover(string $alias): bool return in_array(strtolower($alias), $this->aliases, true); } + + public static function __set_state(array $array) + { + $obj = new static(); + + $properties = array_keys(get_object_vars($obj)); + + foreach ($properties as $property) { + $obj->{$property} = $array[$property]; + } + + return $obj; + } } From db612fab7c62cb0db7305d92f5ac6d0c22f6cb45 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Jul 2023 17:59:58 +0900 Subject: [PATCH 468/485] refactor: move definition of ENVIRONMENT to index.php/spark For Config caching. --- public/index.php | 5 +++++ spark | 5 +++++ system/CodeIgniter.php | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/public/index.php b/public/index.php index d031ea10944a..0cdc61b05ddd 100644 --- a/public/index.php +++ b/public/index.php @@ -43,6 +43,11 @@ require_once SYSTEMPATH . 'Config/DotEnv.php'; (new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); +// Define ENVIRONMENT +if (! defined('ENVIRONMENT')) { + define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production')); +} + /* * --------------------------------------------------------------- * GRAB OUR CODEIGNITER INSTANCE diff --git a/spark b/spark index f2ba3f305ceb..2ea79d5ccdaf 100755 --- a/spark +++ b/spark @@ -78,6 +78,11 @@ require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstra require_once SYSTEMPATH . 'Config/DotEnv.php'; (new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); +// Define ENVIRONMENT +if (! defined('ENVIRONMENT')) { + define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production')); +} + // Grab our CodeIgniter $app = Config\Services::codeigniter(); $app->initialize(); diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 8bdcc15bc16e..3664c0321fa2 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -200,7 +200,6 @@ public function __construct(App $config) public function initialize() { // Define environment variables - $this->detectEnvironment(); $this->bootstrapEnvironment(); // Setup Exception Handling @@ -560,6 +559,8 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache * production * * @codeCoverageIgnore + * + * @deprecated 4.4.0 No longer used. Moved to index.php and spark. */ protected function detectEnvironment() { From 9e4225b8e7e3381824b73be6a844436f897902db Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Jul 2023 18:02:40 +0900 Subject: [PATCH 469/485] feat: add property to stop Config property override --- system/Config/BaseConfig.php | 41 +++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index fad688ab6214..127f70f13667 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -39,6 +39,11 @@ class BaseConfig */ public static $registrars = []; + /** + * Whether to override properties by Env vars and Registrars. + */ + public static bool $override = true; + /** * Has module discovery happened yet? * @@ -78,23 +83,25 @@ public function __construct() { static::$moduleConfig = config(Modules::class); - $this->registerProperties(); - - $properties = array_keys(get_object_vars($this)); - $prefix = static::class; - $slashAt = strrpos($prefix, '\\'); - $shortPrefix = strtolower(substr($prefix, $slashAt === false ? 0 : $slashAt + 1)); - - foreach ($properties as $property) { - $this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix); - - if ($this instanceof Encryption && $property === 'key') { - if (strpos($this->{$property}, 'hex2bin:') === 0) { - // Handle hex2bin prefix - $this->{$property} = hex2bin(substr($this->{$property}, 8)); - } elseif (strpos($this->{$property}, 'base64:') === 0) { - // Handle base64 prefix - $this->{$property} = base64_decode(substr($this->{$property}, 7), true); + if (static::$override) { + $this->registerProperties(); + + $properties = array_keys(get_object_vars($this)); + $prefix = static::class; + $slashAt = strrpos($prefix, '\\'); + $shortPrefix = strtolower(substr($prefix, $slashAt === false ? 0 : $slashAt + 1)); + + foreach ($properties as $property) { + $this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix); + + if ($this instanceof Encryption && $property === 'key') { + if (strpos($this->{$property}, 'hex2bin:') === 0) { + // Handle hex2bin prefix + $this->{$property} = hex2bin(substr($this->{$property}, 8)); + } elseif (strpos($this->{$property}, 'base64:') === 0) { + // Handle base64 prefix + $this->{$property} = base64_decode(substr($this->{$property}, 7), true); + } } } } From b01bb38d8609930ec208cda04ecd94f02d440d4e Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Jul 2023 18:04:05 +0900 Subject: [PATCH 470/485] refactor: use config() instead of new keyword --- system/Config/Services.php | 2 +- system/HTTP/UserAgent.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Config/Services.php b/system/Config/Services.php index ffca4fa804ff..6bd16cc30213 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -117,7 +117,7 @@ public static function cache(?Cache $config = null, bool $getShared = true) return static::getSharedInstance('cache', $config); } - $config ??= new Cache(); + $config ??= config(Cache::class); return CacheFactory::getHandler($config); } diff --git a/system/HTTP/UserAgent.php b/system/HTTP/UserAgent.php index 4b579229d88f..1ccc2109c119 100644 --- a/system/HTTP/UserAgent.php +++ b/system/HTTP/UserAgent.php @@ -102,7 +102,7 @@ class UserAgent */ public function __construct(?UserAgents $config = null) { - $this->config = $config ?? new UserAgents(); + $this->config = $config ?? config(UserAgents::class); if (isset($_SERVER['HTTP_USER_AGENT'])) { $this->agent = trim($_SERVER['HTTP_USER_AGENT']); From 47af9c0cbd91010bbcf760016a8ff8f9fd7ac4d9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Jul 2023 19:45:37 +0900 Subject: [PATCH 471/485] docs: add Config caching code as comments --- public/index.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/index.php b/public/index.php index 0cdc61b05ddd..1cc4710549d5 100644 --- a/public/index.php +++ b/public/index.php @@ -48,6 +48,11 @@ define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production')); } +// Load Config Cache +// $factoriesCache = new \CodeIgniter\Cache\FactoriesCache(); +// $factoriesCache->load('config'); +// ^^^ Uncomment these lines if you want to use Config Caching. + /* * --------------------------------------------------------------- * GRAB OUR CODEIGNITER INSTANCE @@ -73,6 +78,10 @@ $app->run(); +// Save Config Cache +// $factoriesCache->save('config'); +// ^^^ Uncomment this line if you want to use Config Caching. + // Exits the application, setting the exit code for CLI-based applications // that might be watching. exit(EXIT_SUCCESS); From f49a5575339ff59d41c5a9c34198923ada80935f Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Jul 2023 13:04:16 +0900 Subject: [PATCH 472/485] chore: update psalm-baseline.xml --- psalm-baseline.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index dcef743f607f..3ede420d002c 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,10 @@ + + + $val + + Memcache From eb59d2bf7e814be9a5179c944b7d1fc846fac539 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Jul 2023 13:00:14 +0900 Subject: [PATCH 473/485] refactor: by rector --- system/Config/Factories.php | 6 ++---- tests/system/Config/FactoriesTest.php | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 943c9055c982..ae3003c10893 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -467,12 +467,10 @@ public static function getComponentInstances(string $component): array ]; } - $data = [ + return [ 'aliases' => static::$aliases[$component], 'instances' => self::$instances[$component], ]; - - return $data; } /** @@ -494,6 +492,6 @@ public static function setComponentInstances(string $component, array $data) */ public static function isUpdated(string $component): bool { - return isset(self::$updated[$component]) ? true : false; + return isset(self::$updated[$component]); } } diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index d48a6e50141d..c5936a7c341f 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Config; use CodeIgniter\Test\CIUnitTestCase; +use Config\Database; use InvalidArgumentException; use ReflectionClass; use stdClass; @@ -402,7 +403,7 @@ public function testDefineAndLoad() public function testGetComponentInstances() { Factories::config('App'); - Factories::config(\Config\Database::class); + Factories::config(Database::class); $data = Factories::getComponentInstances('config'); From 1e819804ce7d489e4d63cb3bd491d37f196af4eb Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 14 Jul 2023 08:33:19 +0900 Subject: [PATCH 474/485] refactor: remove $val --- system/Cache/FactoriesCache/FileVarExportHandler.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/system/Cache/FactoriesCache/FileVarExportHandler.php b/system/Cache/FactoriesCache/FileVarExportHandler.php index 99a71d8bd23f..f7cee5ef6248 100644 --- a/system/Cache/FactoriesCache/FileVarExportHandler.php +++ b/system/Cache/FactoriesCache/FileVarExportHandler.php @@ -24,7 +24,7 @@ public function save(string $key, $val): void // Write to temp file first to ensure atomicity $tmp = $this->path . "/{$key}." . uniqid('', true) . '.tmp'; - file_put_contents($tmp, 'path . "/{$key}"); } @@ -39,8 +39,6 @@ public function delete(string $key): void */ public function get(string $key) { - @include $this->path . "/{$key}"; - - return $val ?? false; // @phpstan-ignore-line + return @include $this->path . "/{$key}"; } } From 0404ae9886ddd1d5c313c52b41bd198300c31355 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 14 Jul 2023 16:20:11 +0900 Subject: [PATCH 475/485] docs: fix @param Co-authored-by: Andrey Pyzhikov <5071@mail.ru> --- system/Cache/FactoriesCache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Cache/FactoriesCache.php b/system/Cache/FactoriesCache.php index 7644ee5afbde..93114eca10b9 100644 --- a/system/Cache/FactoriesCache.php +++ b/system/Cache/FactoriesCache.php @@ -22,7 +22,7 @@ final class FactoriesCache private $cache; /** - * @param CacheInterface|FileVarExportHandler $cache + * @param CacheInterface|FileVarExportHandler|null $cache */ public function __construct($cache = null) { From b8a42865748950682d60a4a7e76940b9bf5e9e0b Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Jul 2023 18:12:49 +0900 Subject: [PATCH 476/485] refactor: remove __set_state() in Config\Paths No longer use config(Config\Paths::class). --- app/Config/Paths.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/Config/Paths.php b/app/Config/Paths.php index 35a941940b5e..262c745ffa95 100644 --- a/app/Config/Paths.php +++ b/app/Config/Paths.php @@ -77,17 +77,4 @@ class Paths * is used when no value is provided to `Services::renderer()`. */ public string $viewDirectory = __DIR__ . '/../Views'; - - public static function __set_state(array $array) - { - $obj = new self(); - - $properties = array_keys(get_object_vars($obj)); - - foreach ($properties as $property) { - $obj->{$property} = $array[$property]; - } - - return $obj; - } } From 8c2fdc65d4eded5f62713b0096ba805ee98961f6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Jul 2023 17:12:37 +0900 Subject: [PATCH 477/485] refactor: add return types --- system/Cache/FactoriesCache.php | 2 +- system/Config/Factories.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Cache/FactoriesCache.php b/system/Cache/FactoriesCache.php index 93114eca10b9..d78d0b1b02a4 100644 --- a/system/Cache/FactoriesCache.php +++ b/system/Cache/FactoriesCache.php @@ -40,7 +40,7 @@ public function save(string $component): void $this->cache->save($this->getCacheKey($component), $data, 3600 * 24); } - private function getCacheKey(string $component) + private function getCacheKey(string $component): string { return 'FactoriesCache_' . $component; } diff --git a/system/Config/Factories.php b/system/Config/Factories.php index ae3003c10893..0eb6d2443e44 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -478,7 +478,7 @@ public static function getComponentInstances(string $component): array * * @internal For caching only */ - public static function setComponentInstances(string $component, array $data) + public static function setComponentInstances(string $component, array $data): void { static::$aliases[$component] = $data['aliases']; self::$instances[$component] = $data['instances']; From c78f8dd543d6e1c003c650679ffaafe1263216a9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 29 Jul 2023 15:44:15 +0900 Subject: [PATCH 478/485] test: fix out-of-dated test code --- .../Cache/FactoriesCacheFileVarExportHandlerTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php index 3fcf751b6176..a3aff35b000e 100644 --- a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php +++ b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Config\Factories; use CodeIgniter\Test\CIUnitTestCase; use Config\App; +use Config\Modules; /** * @internal @@ -55,10 +56,10 @@ public function testSave() $cachedData = $this->handler->get('FactoriesCache_config'); - $this->assertArrayHasKey('basenames', $cachedData); + $this->assertArrayHasKey('aliases', $cachedData); $this->assertArrayHasKey('instances', $cachedData); - $this->assertArrayHasKey('Modules', $cachedData['basenames']); - $this->assertArrayHasKey('App', $cachedData['basenames']); + $this->assertArrayHasKey(Modules::class, $cachedData['aliases']); + $this->assertArrayHasKey('App', $cachedData['aliases']); } public function testLoad() From 8de664f831db9989b5263c7a3d1d6feee9fae39a Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 15 Aug 2023 12:00:50 +0900 Subject: [PATCH 479/485] docs: add docs --- user_guide_src/source/changelogs/v4.4.0.rst | 6 +- user_guide_src/source/concepts/factories.rst | 78 +++++++++++++++++++ .../source/installation/upgrade_440.rst | 10 ++- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 4b7b065cd6e2..c510f884606d 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -222,8 +222,10 @@ Others - It can also take an object that implements ``ResponseInterface`` as its first argument. - It implements ``ResponsableInterface``. - **DebugBar:** Now :ref:`view-routes` are displayed in *DEFINED ROUTES* on the *Routes* tab. -- **Factories:** You can now define the classname that will actually be loaded. - See :ref:`factories-defining-classname-to-be-loaded`. +- **Factories:** + - You can now define the classname that will actually be loaded. + See :ref:`factories-defining-classname-to-be-loaded`. + - The Config Caching implemented. See :ref:`factories-config-caching` for details. Message Changes *************** diff --git a/user_guide_src/source/concepts/factories.rst b/user_guide_src/source/concepts/factories.rst index 881f0fa1b1f1..aa9c49c16220 100644 --- a/user_guide_src/source/concepts/factories.rst +++ b/user_guide_src/source/concepts/factories.rst @@ -262,3 +262,81 @@ that single call will return a new or shared instance: .. literalinclude:: factories/007.php :lines: 2- + +.. _factories-config-caching: + +Config Caching +************** + +.. versionadded:: 4.4.0 + +To improve performance, the Config Caching has been implemented. + +Prerequisite +============ + +.. important:: Using this feature when the prerequisites are not met will prevent + CodeIgniter from operating properly. Do not use this feature in such cases. + +- To use this feature, the properties of all Config objects instantiated in + Factories must not be modified after instantiation. Put another way, the Config + classes must be an immutable or readonly classes. +- By default, every Config class that is cached must implement ``__set_state()`` + method. + +How It Works +============ + +- Save the all Config instances in Factories into a cache file before shutdown, + if the state of the Config instances in Factories changes. +- Restore cached Config instances before CodeIgniter initialization if a cache + is available. + +Simply put, all Config instances held by Factories are cached immediately prior +to shutdown, and the cached instances are used permanently. + +How to Update Config Values +=========================== + +Once cached, the cache is never expired. Changing a existing Config file +(or changing Environment Variables for it) will not update the cache nor the Config +values. + +So if you want to update Config values, update Config files or Environment Variables +for them, and you must manually delete the cache file. + +You can use the ``spark cache:clear`` command: + +.. code-block:: console + + php spark cache:clear + +Or simply delete the **writable/cache/FactoriesCache_config** file. + +How to Enable Config Caching +============================ + +Uncomment the following code in **public/index.php**:: + + --- a/public/index.php + +++ b/public/index.php + @@ -49,8 +49,8 @@ if (! defined('ENVIRONMENT')) { + } + + // Load Config Cache + -// $factoriesCache = new \CodeIgniter\Cache\FactoriesCache(); + -// $factoriesCache->load('config'); + +$factoriesCache = new \CodeIgniter\Cache\FactoriesCache(); + +$factoriesCache->load('config'); + // ^^^ Uncomment these lines if you want to use Config Caching. + + /* + @@ -79,7 +79,7 @@ $app->setContext($context); + $app->run(); + + // Save Config Cache + -// $factoriesCache->save('config'); + +$factoriesCache->save('config'); + // ^^^ Uncomment this line if you want to use Config Caching. + + // Exits the application, setting the exit code for CLI-based applications diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index ffb8751c5206..1d7eeca2d9f8 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -117,15 +117,16 @@ match the new array structure. Mandatory File Changes ********************** -index.php -========= +index.php and spark +=================== -The following file received significant changes and +The following files received significant changes and **you must merge the updated versions** with your application: - ``public/index.php`` (see also :ref:`v440-codeigniter-and-exit`) +- ``spark`` -.. important:: If you don't update the above file, CodeIgniter will not work +.. important:: If you don't update the above files, CodeIgniter will not work properly after running ``composer update``. The upgrade procedure, for example, is as follows: @@ -134,6 +135,7 @@ The following file received significant changes and composer update cp vendor/codeigniter4/framework/public/index.php public/index.php + cp vendor/codeigniter4/framework/spark spark Config Files ============ From 82328c4d07080797903c0698d413f4650080765d Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 16 Aug 2023 14:23:36 +0900 Subject: [PATCH 480/485] docs: fix by proofreading Co-authored-by: MGatner --- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- user_guide_src/source/concepts/factories.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index c510f884606d..2be5278fe786 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -225,7 +225,7 @@ Others - **Factories:** - You can now define the classname that will actually be loaded. See :ref:`factories-defining-classname-to-be-loaded`. - - The Config Caching implemented. See :ref:`factories-config-caching` for details. + - Config Caching implemented. See :ref:`factories-config-caching` for details. Message Changes *************** diff --git a/user_guide_src/source/concepts/factories.rst b/user_guide_src/source/concepts/factories.rst index aa9c49c16220..f4872327f082 100644 --- a/user_guide_src/source/concepts/factories.rst +++ b/user_guide_src/source/concepts/factories.rst @@ -270,7 +270,7 @@ Config Caching .. versionadded:: 4.4.0 -To improve performance, the Config Caching has been implemented. +To improve performance, Config Caching has been implemented. Prerequisite ============ @@ -298,7 +298,7 @@ to shutdown, and the cached instances are used permanently. How to Update Config Values =========================== -Once cached, the cache is never expired. Changing a existing Config file +Once stored, the cached versions never expire. Changing a existing Config file (or changing Environment Variables for it) will not update the cache nor the Config values. From ef045ce010fa9a2916849df0fc215f5372224e8c Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 17 Aug 2023 10:27:54 +0900 Subject: [PATCH 481/485] refactor: early return --- system/Config/BaseConfig.php | 40 +++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 127f70f13667..d2c396dc36ba 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -83,25 +83,27 @@ public function __construct() { static::$moduleConfig = config(Modules::class); - if (static::$override) { - $this->registerProperties(); - - $properties = array_keys(get_object_vars($this)); - $prefix = static::class; - $slashAt = strrpos($prefix, '\\'); - $shortPrefix = strtolower(substr($prefix, $slashAt === false ? 0 : $slashAt + 1)); - - foreach ($properties as $property) { - $this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix); - - if ($this instanceof Encryption && $property === 'key') { - if (strpos($this->{$property}, 'hex2bin:') === 0) { - // Handle hex2bin prefix - $this->{$property} = hex2bin(substr($this->{$property}, 8)); - } elseif (strpos($this->{$property}, 'base64:') === 0) { - // Handle base64 prefix - $this->{$property} = base64_decode(substr($this->{$property}, 7), true); - } + if (! static::$override) { + return; + } + + $this->registerProperties(); + + $properties = array_keys(get_object_vars($this)); + $prefix = static::class; + $slashAt = strrpos($prefix, '\\'); + $shortPrefix = strtolower(substr($prefix, $slashAt === false ? 0 : $slashAt + 1)); + + foreach ($properties as $property) { + $this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix); + + if ($this instanceof Encryption && $property === 'key') { + if (strpos($this->{$property}, 'hex2bin:') === 0) { + // Handle hex2bin prefix + $this->{$property} = hex2bin(substr($this->{$property}, 8)); + } elseif (strpos($this->{$property}, 'base64:') === 0) { + // Handle base64 prefix + $this->{$property} = base64_decode(substr($this->{$property}, 7), true); } } } From 8d2d9f188b7311f536d59200c88472c69006d7a4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 19 Aug 2023 06:47:30 +0900 Subject: [PATCH 482/485] docs: improve description --- user_guide_src/source/changelogs/v4.4.0.rst | 13 +++++++++++-- user_guide_src/source/installation/upgrade_440.rst | 6 +++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index a6a3bdde7d88..47d3b4e270a2 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -34,8 +34,8 @@ The next segment (``+1``) of the current last segment can be set as before. Factories --------- -Passing Fully Qualified Classname -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Passing Classname with Namespace +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Now ``preferApp`` works only when you request :ref:`a classname without a namespace `. @@ -53,6 +53,15 @@ For example, when you call ``model(\Myth\Auth\Models\UserModel::class)`` or - returns ``Myth\Auth\Models\UserModel`` even if ``preferApp`` is true (default) - returns ``App\Models\UserModel`` if you define ``Factories::define('models', 'Myth\Auth\Models\UserModel', 'App\Models\UserModel')`` before calling the ``model()`` +If you had passed a non-existent classname by mistake, the previous version +would have returned a class instance in the ``App`` or ``Config`` namespace +because of the ``preferApp`` feature. + +For example, in a controller (``namespace App\Controllers``), if you call +``config(Config\App::class)`` by mistake (note that you forgot the leading ``\`` +in ``Config\App::class``), that means you pass ``App\Controllers\Config\App``. +But the class does not exist, so now Factories return ``null``. + Property Name ^^^^^^^^^^^^^ diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index c4bdfe0c81ff..3c34b0241746 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -85,10 +85,10 @@ This bug was fixed and now URIs for underscores (**foo_bar**) is not accessible. If you have links to URIs for underscores (**foo_bar**), update them with URIs for dashes (**foo-bar**). -When Passing Fully Qualified Classnames to Factories -==================================================== +When Passing Classname with Namespace to Factories +================================================== -The behavior of passing fully qualified classnames to Factories has been changed. +The behavior of passing a classname with a namespace to Factories has been changed. See :ref:`ChangeLog ` for details. If you have code like ``model('\Myth\Auth\Models\UserModel::class')`` or From 5ba1b9cf4433622486e2977183c35a32ed77521c Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 21 Aug 2023 09:48:57 +0900 Subject: [PATCH 483/485] docs: fix by proofreading Co-authored-by: MGatner --- user_guide_src/source/changelogs/v4.4.0.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 47d3b4e270a2..4f7615c42e3f 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -57,10 +57,10 @@ If you had passed a non-existent classname by mistake, the previous version would have returned a class instance in the ``App`` or ``Config`` namespace because of the ``preferApp`` feature. -For example, in a controller (``namespace App\Controllers``), if you call -``config(Config\App::class)`` by mistake (note that you forgot the leading ``\`` -in ``Config\App::class``), that means you pass ``App\Controllers\Config\App``. -But the class does not exist, so now Factories return ``null``. +For example, in a controller (``namespace App\Controllers``), if you called +``config(Config\App::class)`` by mistake (note the class is missing the leading ``\``), +meaning you actually passed ``App\Controllers\Config\App``. +But that class does not exist, so now Factories will return ``null``. Property Name ^^^^^^^^^^^^^ From 27ade011a8ed7dbf533234fa431c68704d266ffa Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 21 Aug 2023 10:46:35 +0900 Subject: [PATCH 484/485] style: composer cs-fix --- user_guide_src/source/outgoing/response/003.php | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/outgoing/response/003.php b/user_guide_src/source/outgoing/response/003.php index e5bff8cabf2f..da5819d98860 100644 --- a/user_guide_src/source/outgoing/response/003.php +++ b/user_guide_src/source/outgoing/response/003.php @@ -6,5 +6,6 @@ ]; return $this->response->setJSON($data); + // or return $this->response->setXML($data); From f02979bcbe32fd09c55fd6eec1efc58862144b62 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 21 Aug 2023 10:46:55 +0900 Subject: [PATCH 485/485] docs: add @return --- system/Exceptions/FrameworkException.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/Exceptions/FrameworkException.php b/system/Exceptions/FrameworkException.php index d9396712aa0e..4cafd71177e2 100644 --- a/system/Exceptions/FrameworkException.php +++ b/system/Exceptions/FrameworkException.php @@ -47,6 +47,9 @@ public static function forInvalidDirectory(string $path) return new static(lang('Core.invalidDirectory', [$path])); } + /** + * @return static + */ public static function forCopyError(string $path) { return new static(lang('Core.copyError', [$path]));