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', 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(), diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 008a865bcdcd..692cdfaf05e9 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 URIFactory. */ 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 URIFactory. */ 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 URIFactory. */ protected function parseQueryString(): string { @@ -495,6 +501,9 @@ public function setPath(string $path, ?App $config = null) return $this; } + /** + * @deprecated Moved to URIFactory. + */ private function determineHost(App $config, string $baseURL): string { $host = parse_url($baseURL, PHP_URL_HOST); diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 587e441ffc17..fcc90a2391ca 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -43,6 +43,7 @@ class URI /** * List of URI segments. + * URI Segments mean only the URI path part relative to the baseURL. * * Starts at 1 instead of 0 * @@ -97,6 +98,16 @@ class URI */ protected $path; + /** + * URI path relative to baseURL. + * + * If the baseURL contains sub folders, this value will be different from + * the current URI path. + * + * @var string + */ + protected $routePath; + /** * The name of any fragment. * @@ -480,6 +491,20 @@ public function getPath(): string return $this->path ?? ''; } + /** + * Returns the URI path relative to baseURL. + * + * @return string The Route path. + */ + public function getRoutePath(): string + { + if ($this->routePath === null) { + throw new BadMethodCallException('The $routePath is not set.'); + } + + return $this->routePath; + } + /** * Retrieve the query string */ @@ -757,6 +782,22 @@ public function setPath(string $path) return $this; } + /** + * Sets the route path. + * + * @return $this + */ + public function setRoutePath(string $path) + { + $this->routePath = $this->filterPath($path); + + $tempPath = trim($this->routePath, '/'); + + $this->segments = ($tempPath === '') ? [] : explode('/', $tempPath); + + return $this; + } + /** * Sets the current baseURL. * diff --git a/system/HTTP/URIFactory.php b/system/HTTP/URIFactory.php new file mode 100644 index 000000000000..4c423bde7a67 --- /dev/null +++ b/system/HTTP/URIFactory.php @@ -0,0 +1,255 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\Exceptions\ConfigException; +use Config\App; + +class URIFactory +{ + /** + * @var array Superglobal SERVER array + */ + private array $server; + + /** + * @var array Superglobal GET array + */ + private array $get; + + private App $appConfig; + + /** + * @param array $server Superglobal $_SERVER array + * @param array $get Superglobal $_GET array + */ + public function __construct(array &$server, array &$get, App $appConfig) + { + $this->server = &$server; + $this->get = &$get; + $this->appConfig = $appConfig; + } + + /** + * Create the current URI object from superglobals. + * + * This method updates superglobal $_SERVER and $_GET. + */ + public function createFromGlobals(): URI + { + $routePath = $this->detectRoutePath(); + + return $this->createURIFromRoutePath($routePath); + } + + /** + * 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; + } else { + $this->server['QUERY_STRING'] = $query; + } + + // Update our global GET for values likely to have been changed + parse_str($this->server['QUERY_STRING'], $this->get); + + return URI::removeDotSegments($path); + } + + /** + * 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; + } else { + $path = $query; + } + + // Update our global GET for values likely to have been changed + parse_str($this->server['QUERY_STRING'], $this->get); + + return URI::removeDotSegments($path); + } + + /** + * Create current URI object. + * + * @param string $routePath URI path relative to baseURL + */ + private function createURIFromRoutePath(string $routePath): URI + { + $config = $this->appConfig; + + // 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($baseURL); + + // Set URI::$baseURL + $uri = new URI($baseURL); + $currentBaseURL = (string) $uri->setHost($host); + $uri->setBaseURL($currentBaseURL); + + $uri->setPath($routePath); + + $uri->setRoutePath($routePath); + + $uri->setScheme(parse_url($baseURL, PHP_URL_SCHEME)); + $uri->setHost($host); + $uri->setPort(parse_url($baseURL, PHP_URL_PORT)); + + // Ensure we have any query vars + $uri->setQuery($this->server['QUERY_STRING'] ?? ''); + + // Check if the scheme needs to be coerced into its secure version + if ($config->forceGlobalSecureRequests && $uri->getScheme() === 'http') { + $uri->setScheme('https'); + } + + return $uri; + } + if (! is_cli()) { + throw new ConfigException( + 'You have an empty or invalid baseURL. The baseURL value must be set in app/Config/App.php, or through the .env file.' + ); + } + + return new URI(); + } + + /** + * @return string The current hostname. + */ + private function determineHost(string $baseURL): string + { + $host = parse_url($baseURL, PHP_URL_HOST); + + if (empty($this->appConfig->allowedHostnames)) { + return $host; + } + + // Update host if it is valid. + $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/URIFactoryDetectRoutePathTest.php b/tests/system/HTTP/URIFactoryDetectRoutePathTest.php new file mode 100644 index 000000000000..b695cd6fbccd --- /dev/null +++ b/tests/system/HTTP/URIFactoryDetectRoutePathTest.php @@ -0,0 +1,248 @@ + + * + * 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 URIFactoryDetectRoutePathTest extends CIUnitTestCase +{ + private function createURIFactory(array &$server, array &$get, ?App $appConfig = null): URIFactory + { + $appConfig ??= new App(); + + return new URIFactory($server, $get, $appConfig); + } + + public function testDefault() + { + $_GET = $_SERVER = []; + + // /index.php/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createURIFactory($_SERVER, $_GET); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath()); + } + + public function testDefaultEmpty() + { + $_GET = $_SERVER = []; + + // / + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createURIFactory($_SERVER, $_GET); + + $expected = '/'; + $this->assertSame($expected, $factory->detectRoutePath()); + } + + public function testRequestURI() + { + $_GET = $_SERVER = []; + + // /index.php/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createURIFactory($_SERVER, $_GET); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURINested() + { + $_GET = $_SERVER = []; + + // 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->createURIFactory($_SERVER, $_GET); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURISubfolder() + { + $_GET = $_SERVER = []; + + // /ci/index.php/popcorn/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/ci/index.php/popcorn/woot'; + $_SERVER['SCRIPT_NAME'] = '/ci/index.php'; + + $factory = $this->createURIFactory($_SERVER, $_GET); + + $expected = 'popcorn/woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURINoIndex() + { + $_GET = $_SERVER = []; + + // /sub/example + $_SERVER['REQUEST_URI'] = '/sub/example'; + $_SERVER['SCRIPT_NAME'] = '/sub/index.php'; + + $factory = $this->createURIFactory($_SERVER, $_GET); + + $expected = 'example'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURINginx() + { + $_GET = $_SERVER = []; + + // /ci/index.php/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php/woot?code=good'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createURIFactory($_SERVER, $_GET); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURINginxRedirecting() + { + $_GET = $_SERVER = []; + + // /?/ci/index.php/woot + $_SERVER['REQUEST_URI'] = '/?/ci/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createURIFactory($_SERVER, $_GET); + + $expected = 'ci/woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testRequestURISuppressed() + { + $_GET = $_SERVER = []; + + // /woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/woot'; + $_SERVER['SCRIPT_NAME'] = '/'; + + $factory = $this->createURIFactory($_SERVER, $_GET); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + public function testQueryString() + { + $_GET = $_SERVER = []; + + // /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->createURIFactory($_SERVER, $_GET); + + $expected = 'ci/woot'; + $this->assertSame($expected, $factory->detectRoutePath('QUERY_STRING')); + } + + public function testQueryStringWithQueryString() + { + $_GET = $_SERVER = []; + + // /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->createURIFactory($_SERVER, $_GET); + + $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() + { + $_GET = $_SERVER = []; + + // /index.php? + $_SERVER['REQUEST_URI'] = '/index.php?'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createURIFactory($_SERVER, $_GET); + + $expected = '/'; + $this->assertSame($expected, $factory->detectRoutePath('QUERY_STRING')); + } + + public function testPathInfoUnset() + { + $_GET = $_SERVER = []; + + // /index.php/woot?code=good#pos + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $factory = $this->createURIFactory($_SERVER, $_GET); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('PATH_INFO')); + } + + public function testPathInfoSubfolder() + { + $_GET = $_SERVER = []; + + $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->createURIFactory($_SERVER, $_GET, $appConfig); + + $expected = 'woot'; + $this->assertSame($expected, $factory->detectRoutePath('PATH_INFO')); + } +} diff --git a/tests/system/HTTP/URIFactoryTest.php b/tests/system/HTTP/URIFactoryTest.php new file mode 100644 index 000000000000..e20d853aa9a2 --- /dev/null +++ b/tests/system/HTTP/URIFactoryTest.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\Config\Factories; +use CodeIgniter\Test\CIUnitTestCase; +use Config\App; + +/** + * @backupGlobals enabled + * + * @internal + * + * @group Others + */ +final class URIFactoryTest extends CIUnitTestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $_GET = $_SERVER = []; + } + + protected function tearDown(): void + { + Factories::reset('config'); + } + + public function testCreateCurrentURI() + { + // 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 URIFactory($_SERVER, $_GET, new App()); + + $uri = $factory->createFromGlobals(); + + $this->assertInstanceOf(URI::class, $uri); + $this->assertSame('http://localhost:8080/woot?code=good', (string) $uri); + $this->assertSame('woot', $uri->getPath()); + $this->assertSame('woot', $uri->getRoutePath()); + } +} 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);