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 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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 ************