diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index 122db5f9a9f2..4063ad1fd05a 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -3,8 +3,6 @@ namespace App\Controllers; use CodeIgniter\Controller; -use CodeIgniter\HTTP\CLIRequest; -use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Psr\Log\LoggerInterface; @@ -24,7 +22,7 @@ abstract class BaseController extends Controller /** * Instance of the main Request object. * - * @var CLIRequest|IncomingRequest + * @var RequestInterface */ protected $request; diff --git a/phpstan-baseline.neon.dist b/phpstan-baseline.neon.dist index d2fdbd95b04f..4967937c820e 100644 --- a/phpstan-baseline.neon.dist +++ b/phpstan-baseline.neon.dist @@ -270,11 +270,6 @@ parameters: count: 1 path: system/HTTP/Request.php - - - message: "#^Property CodeIgniter\\\\HTTP\\\\Request\\:\\:\\$uri \\(CodeIgniter\\\\HTTP\\\\URI\\) in empty\\(\\) is not falsy\\.$#" - count: 1 - path: system/HTTP/Request.php - - message: "#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$fragment \\(string\\) on left side of \\?\\? is not nullable\\.$#" count: 1 @@ -459,3 +454,8 @@ parameters: message: "#^Property Config\\\\View\\:\\:\\$plugins \\(array\\) on left side of \\?\\? is not nullable\\.$#" count: 1 path: system/View/Parser.php + + - + message: "#^Constructor of class CodeIgniter\\\\HTTP\\\\CURLRequest has an unused parameter \\$config\\.$#" + count: 1 + path: system/HTTP/CURLRequest.php diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index ba353176a74c..ea4fc6bd67a9 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -19,7 +19,7 @@ /** * A lightweight HTTP client for sending synchronous HTTP requests via cURL. */ -class CURLRequest extends Request +class CURLRequest extends OutgoingRequest { /** * The response object associated with this request @@ -103,7 +103,7 @@ public function __construct(App $config, URI $uri, ?ResponseInterface $response throw HTTPException::forMissingCurl(); // @codeCoverageIgnore } - parent::__construct($config); + parent::__construct('GET', $uri); $this->response = $response; $this->baseURI = $uri->useRawQueryString(); diff --git a/system/HTTP/Message.php b/system/HTTP/Message.php index 34181d5aec60..01496d780c1a 100644 --- a/system/HTTP/Message.php +++ b/system/HTTP/Message.php @@ -61,6 +61,8 @@ public function getBody() * * @deprecated Use Message::headers() to make room for PSR-7 * + * @TODO Incompatible return value with PSR-7 + * * @codeCoverageIgnore */ public function getHeaders(): array @@ -76,6 +78,8 @@ public function getHeaders(): array * * @deprecated Use Message::header() to make room for PSR-7 * + * @TODO Incompatible return value with PSR-7 + * * @codeCoverageIgnore */ public function getHeader(string $name) diff --git a/system/HTTP/OutgoingRequest.php b/system/HTTP/OutgoingRequest.php new file mode 100644 index 000000000000..6a339038ea0c --- /dev/null +++ b/system/HTTP/OutgoingRequest.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +/** + * Representation of an outgoing, client-side request. + */ +class OutgoingRequest extends Message implements OutgoingRequestInterface +{ + /** + * Request method. + * + * @var string + */ + protected $method; + + /** + * A URI instance. + * + * @var URI|null + */ + protected $uri; + + /** + * @param string $method HTTP method + * @param string|null $body + */ + public function __construct( + string $method, + ?URI $uri = null, + array $headers = [], + $body = null, + string $version = '1.1' + ) { + $this->method = $method; + $this->uri = $uri; + + foreach ($headers as $header => $value) { + $this->setHeader($header, $value); + } + + $this->body = $body; + $this->protocolVersion = $version; + + if (! $this->hasHeader('Host') && $this->uri->getHost() !== '') { + $this->setHeader('Host', $this->getHostFromUri($this->uri)); + } + } + + private function getHostFromUri(URI $uri): string + { + $host = $uri->getHost(); + + return $host . ($uri->getPort() ? ':' . $uri->getPort() : ''); + } + + /** + * Get the request method. + * + * @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 + */ + public function getMethod(bool $upper = false): string + { + return ($upper) ? strtoupper($this->method) : strtolower($this->method); + } + + /** + * Sets the request method. Used when spoofing the request. + * + * @return $this + * + * @deprecated Use withMethod() instead for immutability + */ + public function setMethod(string $method) + { + $this->method = $method; + + return $this; + } + + /** + * Returns an instance with the specified method. + * + * @param string $method + * + * @return static + */ + public function withMethod($method) + { + $request = clone $this; + $request->method = $method; + + return $request; + } + + /** + * Retrieves the URI instance. + * + * @return URI|null + */ + public function getUri() + { + return $this->uri; + } + + /** + * Returns an instance with the provided URI. + * + * @param URI $uri New request URI to use. + * @param bool $preserveHost Preserve the original state of the Host header. + * + * @return static + */ + public function withUri(URI $uri, $preserveHost = false) + { + $request = clone $this; + $request->uri = $uri; + + if ($preserveHost) { + if ($this->isHostHeaderMissingOrEmpty() && $uri->getHost() !== '') { + $request->setHeader('Host', $this->getHostFromUri($uri)); + + return $request; + } + + if ($this->isHostHeaderMissingOrEmpty() && $uri->getHost() === '') { + return $request; + } + + if (! $this->isHostHeaderMissingOrEmpty()) { + return $request; + } + } + + if ($uri->getHost() !== '') { + $request->setHeader('Host', $this->getHostFromUri($uri)); + } + + return $request; + } + + private function isHostHeaderMissingOrEmpty(): bool + { + if (! $this->hasHeader('Host')) { + return true; + } + + return $this->header('Host')->getValue() === ''; + } +} diff --git a/system/HTTP/OutgoingRequestInterface.php b/system/HTTP/OutgoingRequestInterface.php new file mode 100644 index 000000000000..3839b64fd8e1 --- /dev/null +++ b/system/HTTP/OutgoingRequestInterface.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use InvalidArgumentException; + +/** + * Representation of an outgoing, client-side request. + * + * Corresponds to Psr7\RequestInterface. + */ +interface OutgoingRequestInterface extends MessageInterface +{ + /** + * Get the request method. + * An extension of PSR-7's getMethod to allow casing. + * + * @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 + */ + public function getMethod(bool $upper = false): string; + + /** + * Return an instance with the provided HTTP method. + * + * While HTTP method names are typically all uppercase characters, HTTP + * method names are case-sensitive and thus implementations SHOULD NOT + * modify the given string. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request method. + * + * @param string $method Case-sensitive method. + * + * @return static + * + * @throws InvalidArgumentException for invalid HTTP methods. + */ + public function withMethod($method); + + /** + * Retrieves the URI instance. + * + * @see http://tools.ietf.org/html/rfc3986#section-4.3 + * + * @return URI + */ + public function getUri(); + + /** + * Returns an instance with the provided URI. + * + * This method MUST update the Host header of the returned request by + * default if the URI contains a host component. If the URI does not + * contain a host component, any pre-existing Host header MUST be carried + * over to the returned request. + * + * You can opt-in to preserving the original state of the Host header by + * setting `$preserveHost` to `true`. When `$preserveHost` is set to + * `true`, this method interacts with the Host header in the following ways: + * + * - If the Host header is missing or empty, and the new URI contains + * a host component, this method MUST update the Host header in the returned + * request. + * - If the Host header is missing or empty, and the new URI does not contain a + * host component, this method MUST NOT update the Host header in the returned + * request. + * - If a Host header is present and non-empty, this method MUST NOT update + * the Host header in the returned request. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new UriInterface instance. + * + * @see http://tools.ietf.org/html/rfc3986#section-4.3 + * + * @param URI $uri New request URI to use. + * @param bool $preserveHost Preserve the original state of the Host header. + * + * @return static + */ + public function withUri(URI $uri, $preserveHost = false); +} diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index 9abed1e28388..fecc2c5c7b08 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -12,11 +12,12 @@ namespace CodeIgniter\HTTP; use CodeIgniter\Validation\FormatRules; +use Config\App; /** - * Representation of an HTTP request. + * Representation of an incoming, server-side HTTP request. */ -class Request extends Message implements RequestInterface +class Request extends OutgoingRequest implements RequestInterface { use RequestTrait; @@ -29,24 +30,10 @@ class Request extends Message implements RequestInterface */ protected $proxyIPs; - /** - * Request method. - * - * @var string - */ - protected $method; - - /** - * A URI instance. - * - * @var URI - */ - protected $uri; - /** * Constructor. * - * @param object $config + * @param App $config * * @deprecated The $config is no longer needed and will be removed in a future version */ diff --git a/system/HTTP/RequestInterface.php b/system/HTTP/RequestInterface.php index 7e9ed0a74209..fd2cdfbbc949 100644 --- a/system/HTTP/RequestInterface.php +++ b/system/HTTP/RequestInterface.php @@ -12,9 +12,11 @@ namespace CodeIgniter\HTTP; /** - * Expected behavior of an HTTP request + * Representation of an incoming, server-side HTTP request. + * + * Corresponds to Psr7\ServerRequestInterface. */ -interface RequestInterface extends MessageInterface +interface RequestInterface extends OutgoingRequestInterface { /** * Gets the user's IP address. @@ -34,16 +36,6 @@ public function getIPAddress(): string; */ public function isValidIP(string $ip, ?string $which = null): bool; - /** - * Get the request method. - * An extension of PSR-7's getMethod to allow casing. - * - * @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 - */ - public function getMethod(bool $upper = false): string; - /** * Fetch an item from the $_SERVER array. * Supplied by RequestTrait. diff --git a/tests/system/HTTP/OutgoingRequestTest.php b/tests/system/HTTP/OutgoingRequestTest.php new file mode 100644 index 000000000000..b52cdb2f0366 --- /dev/null +++ b/tests/system/HTTP/OutgoingRequestTest.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\HTTP; + +use CodeIgniter\Test\CIUnitTestCase; + +/** + * @internal + */ +final class OutgoingRequestTest extends CIUnitTestCase +{ + public function testCreateWithHeader() + { + $uri = new URI('https://example.com/'); + $headers = ['User-Agent' => 'Mozilla/5.0']; + $request = new OutgoingRequest('GET', $uri, $headers); + + $this->assertSame('Mozilla/5.0', $request->header('User-Agent')->getValue()); + } + + public function testGetUri() + { + $uri = new URI('https://example.com/'); + $request = new OutgoingRequest('GET', $uri); + + $this->assertSame($uri, $request->getUri()); + } + + public function testWithMethod() + { + $uri = new URI('https://example.com/'); + $request = new OutgoingRequest('GET', $uri); + + $newRequest = $request->withMethod('POST'); + + $this->assertSame('GET', strtoupper($request->getMethod())); + $this->assertSame('POST', strtoupper($newRequest->getMethod())); + } + + public function testWithUri() + { + $uri = new URI('https://example.com/'); + $request = new OutgoingRequest('GET', $uri); + + $newUri = new URI('https://example.jp/'); + $newRequest = $request->withUri($newUri); + + $this->assertSame('example.jp', $newRequest->header('Host')->getValue()); + } + + /** + * If the Host header is missing or empty, and the new URI contains + * a host component, this method MUST update the Host header in the returned + * request. + * https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface + */ + public function testWithUriPreserveHostHostHeaderIsMissingAndNewUriContainsHost() + { + $uri = new URI(); + $request = new OutgoingRequest('GET', $uri); + + $newUri = new URI('https://example.com/'); + $newRequest = $request->withUri($newUri, true); + + $this->assertSame('example.com', $newRequest->header('Host')->getValue()); + } + + /** + * If the Host header is missing or empty, and the new URI does not contain a + * host component, this method MUST NOT update the Host header in the returned + * request. + * https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface + */ + public function testWithUriPreserveHostHostHeaderIsMissingAndNewUriDoesNotContainsHost() + { + $uri = new URI(); + $request = new OutgoingRequest('GET', $uri); + + $newUri = new URI(); + $newRequest = $request->withUri($newUri, true); + + $this->assertSame($request->header('Host'), $newRequest->header('Host')); + } + + /** + * If a Host header is present and non-empty, this method MUST NOT update + * the Host header in the returned request. + * https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface + */ + public function testWithUriPreserveHostHostHostIsNonEmpty() + { + $uri = new URI('https://example.com/'); + $request = new OutgoingRequest('GET', $uri); + + $newUri = new URI('https://example.jp/'); + $newRequest = $request->withUri($newUri, true); + + $this->assertSame('example.com', $newRequest->header('Host')->getValue()); + } +} diff --git a/user_guide_src/source/changelogs/v4.3.0.rst b/user_guide_src/source/changelogs/v4.3.0.rst index 022cc0e01392..51fc3b1564cf 100644 --- a/user_guide_src/source/changelogs/v4.3.0.rst +++ b/user_guide_src/source/changelogs/v4.3.0.rst @@ -58,8 +58,23 @@ Others 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. + +OutgoingRequestInterface +------------------------ + +- Added new ``OutgoingRequestInterface`` that represents an outgoing request. +- Added new ``OutgoingRequest`` class that implements ``OutgoingRequestInterface``. +- Now ``RequestInterface`` extends ``OutgoingRequestInterface``. +- Now ``CURLRequest`` extends ``OutgoingRequest``. +- Now ``Request`` extends ``OutgoingRequest``. + +Others +------ + - Added missing ``getProtocolVersion()``, ``getBody()``, ``hasHeader()`` and ``getHeaderLine()`` method in ``MessageInterface``. -- Now ``RequestInterface`` extends ``MessageInterface``. - Now ``ResponseInterface`` extends ``MessageInterface``. - Added missing ``ResponseInterface::getCSP()`` (and ``Response::getCSP()``), ``ResponseInterface::getReasonPhrase()`` and ``ResponseInterface::getCookieStore()`` methods. - Added missing ``CodeIgniter\Database\ResultInterface::getNumRows()`` method.