diff --git a/docs/book/v2/api.md b/docs/book/v2/api.md index e20c22f2..cb641f62 100644 --- a/docs/book/v2/api.md +++ b/docs/book/v2/api.md @@ -194,8 +194,7 @@ and provides a number of facilities around manipulating the composed PHP stream constructor accepts a stream, which may be one of: - A stream identifier; e.g., `php://input`, a filename, etc. -- A PHP stream resource; or -- A GD resource +- A PHP stream resource If a stream identifier is provided, an optional second parameter may be provided, the file mode by which to `fopen` the stream. diff --git a/docs/book/v3/api.md b/docs/book/v3/api.md new file mode 100644 index 00000000..e255b3f3 --- /dev/null +++ b/docs/book/v3/api.md @@ -0,0 +1,214 @@ +# API + +## Request Message + +`Laminas\Diactoros\Request` implements [`Psr\Http\Message\RequestInterface`](https://github.com/php-fig/http-message/blob/master/src/RequestInterface.php), +and is intended for client-side requests. It includes the following methods: + +```php +class Request +{ + public function __construct( + $uri = null, + $method = null, + $body = 'php://memory', + array $headers = [] + ); + + // See psr/http-message's RequestInterface for other methods +} +``` + +Requests are immutable. Any methods that would change state — those prefixed with `with` and +`without` — all return a new instance with the changes requested. + +## ServerRequest Message + +For server-side applications, `Laminas\Diactoros\ServerRequest` implements +[`Psr\Http\Message\ServerRequestInterface`](https://github.com/php-fig/http-message/blob/master/src/ServerRequestInterface.php), +which provides access to the elements of an HTTP request, as well as uniform access to the various +elements of incoming data. The methods included are: + +```php +class ServerRequest +{ + public function __construct( + array $serverParams = [], + array $fileParams = [], + $uri = null, + $method = null, + $body = 'php://input', + array $headers = [] + ); + + // See psr/http-message's ServerRequestInterface for other methods. +} +``` + +The `ServerRequest` is immutable. Any methods that would change state — those prefixed with `with` +and `without` — all return a new instance with the changes requested. Server parameters are +considered completely immutable, however, as they cannot be recalculated, and, rather, is a source +for other values. + +## Response Message + +`Laminas\Diactoros\Response` provides an implementation of +[`Psr\Http\Message\ResponseInterface`](https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php), +an object to be used to aggregate response information for both HTTP clients and server-side +applications, including headers and message body content. It includes the following: + +```php +class Response +{ + public function __construct( + $body = 'php://memory', + $statusCode = 200, + array $headers = [] + ); + + // See psr/http-message's ResponseInterface for other methods +} +``` + +Like the `Request` and `ServerRequest`, responses are immutable. Any methods that would change state +— those prefixed with `with` and `without` — all return a new instance with the changes requested. + +### HtmlResponse and JsonResponse + +The most common use case in server-side applications for generating responses is to provide a string +to use for the response, typically HTML or data to serialize as JSON. `Laminas\Diactoros\Response\HtmlResponse` and `Laminas\Diactoros\Response\JsonResponse` exist to facilitate these use cases: + +```php +$htmlResponse = new HtmlResponse($html); + +$jsonResponse = new JsonResponse($data); +``` + +In the first example, you will receive a response with a stream containing the HTML; additionally, +the `Content-Type` header will be set to `text/html`. In the second case, the stream will contain a +stream containing the JSON-serialized `$data`, and have a `Content-Type` header set to +`application/json`. + +Both objects allow passing the HTTP status, as well as any headers you want to specify, +including the `Content-Type` header: + +```php +$htmlResponse = new HtmlResponse($html, 404, [ + 'Content-Type' => [ 'application/xhtml+xml' ], +]); + +$jsonResponse = new JsonResponse($data, 422, [ + 'Content-Type' => [ 'application/problem+json' ], +]); +``` + +## ServerRequestFactory + +This static class can be used to marshal a `ServerRequest` instance from the PHP environment. +The primary entry point is `Laminas\Diactoros\ServerRequestFactory::fromGlobals(array $server, array $query, array $body, array $cookies, array $files, ?Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface $requestFilter)`. +This method will create a new `ServerRequest` instance with the data provided. +Examples of usage are: + +```php +// Returns new ServerRequest instance, using values from superglobals: +$request = ServerRequestFactory::fromGlobals(); + +// or + +// Returns new ServerRequest instance, using values provided (in this +// case, equivalent to the previous!) +$request = ServerRequestFactory::fromGlobals( + $_SERVER, + $_GET, + $_POST, + $_COOKIE, + $_FILES +); +``` + +### Request Filters + +Since version 2.11.1, this method takes the additional optional argument `$requestFilter`. +This should be a `null` value, or an instance of [`Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface`](server-request-filters.md). +For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders`](server-request-filters.md#filterusingxforwardedheaders) instance configured as follows: + +```php +$requestFilter = $requestFilter ?: FilterUsingXForwardedHeaders::trustReservedSubnets(); +``` + +The request filter is called on the generated server request instance, and its result is returned from `fromGlobals()`. + +**For version 3 releases, this method will switch to using a `Laminas\Diactoros\ServerRequestFilter\DoNotFilter` by default.** +If you are using this factory method directly, please be aware and update your code accordingly. + +### ServerRequestFactory Helper Functions + +In order to create the various artifacts required by a `ServerRequest` instance, +Diactoros also provides a number of functions under the `Laminas\Diactoros` +namespace for introspecting the SAPI `$_SERVER` parameters, headers, `$_FILES`, +and even the `Cookie` header. These include: + +- `Laminas\Diactoros\normalizeServer(array $server, callable $apacheRequestHeaderCallback = null) : array` + (its main purpose is to aggregate the `Authorization` header in the SAPI params + when under Apache) +- `Laminas\Diactoros\marshalProtocolVersionFromSapi(array $server) : string` +- `Laminas\Diactoros\marshalMethodFromSapi(array $server) : string`. +- `Laminas\Diactoros\marshalUriFromSapi(array $server, array $headers) : Uri`. + Please note: **this function is deprecated as of version 2.11.1**, and no longer used in `ServerRequestFactory::fromGlobals()`. + Use `ServerRequestFactory::fromGlobals()` instead. +- `Laminas\Diactoros\marshalHeadersFromSapi(array $server) : array` +- `Laminas\Diactoros\parseCookieHeader(string $header) : array` +- `Laminas\Diactoros\createUploadedFile(array $spec) : UploadedFile` (creates the + instance from a normal `$_FILES` entry) +- `Laminas\Diactoros\normalizeUploadedFiles(array $files) : UploadedFileInterface[]` + (traverses a potentially nested array of uploaded file instances and/or + `$_FILES` entries, including those aggregated under mod_php, php-fpm, and + php-cgi in order to create a flat array of `UploadedFileInterface` instances + to use in a request) + +## URI + +`Laminas\Diactoros\Uri` is an implementation of +[`Psr\Http\Message\UriInterface`](https://github.com/php-fig/http-message/blob/master/src/UriInterface.php), +and models and validates URIs. It implements `__toString()`, allowing it to be represented as a +string and `echo()`'d directly. The following methods are pertinent: + +```php +class Uri +{ + public function __construct($uri = ''); + + // See psr/http-message's UriInterface for other methods. +} +``` + +Like the various message objects, URIs are immutable. Any methods that would +change state — those +prefixed with `with` and `without` — all return a new instance with the changes requested. + +## Stream + +`Laminas\Diactoros\Stream` is an implementation of +[`Psr\Http\Message\StreamInterface`](https://github.com/php-fig/http-message/blob/master/src/StreamInterface.php), +and provides a number of facilities around manipulating the composed PHP stream resource. The +constructor accepts a stream, which may be one of: + +- A stream identifier; e.g., `php://input`, a filename, etc. +- A PHP stream resource; or + +If a stream identifier is provided, an optional second parameter may be provided, the file mode by +which to `fopen` the stream. + +`ServerRequest` objects by default use a `php://input` stream set to read-only; `Response` objects +by default use a `php://memory` with a mode of `wb+`, allowing binary read/write access. + +In most cases, you will not interact with the Stream object directly. + +## UploadedFile + +`Laminas\Diactoros\UploadedFile` is an implementation of +[`Psr\Http\Message\UploadedFileInterface`](https://github.com/php-fig/http-message/blob/master/src/UploadedFileInterface.php), +and provides abstraction around a single uploaded file, including behavior for interacting with it +as a stream or moving it to a filesystem location. + +In most cases, you will only use the methods defined in the `UploadedFileInterface`. diff --git a/docs/book/v3/custom-responses.md b/docs/book/v3/custom-responses.md new file mode 100644 index 00000000..ad0bf5bd --- /dev/null +++ b/docs/book/v3/custom-responses.md @@ -0,0 +1,260 @@ +# Custom Responses + +When developing server-side applications, the message type you're most likely to create manually is +the response. In such cases, the standard signature can be an obstacle to usability. Let's review: + +```php +namespace Laminas\Diactoros; + +use Psr\Http\Message\ResponseInterface; + +class Response implements ResponseInterface +{ + public function __construct($body = 'php://temp', $status = 200, array $headers = []); +} +``` + +Some standard use cases, however, make this un-wieldy: + +- Returning a response containing HTML; in this case, you likely want to provide the HTML to the + constructor, not a stream with the HTML injected. +- Returning a response containing JSON; in this case, you likely want to provide the data to + serialize to JSON, not a stream containing serialized JSON. +- Returning a response with no content; in this case, you don't want to bother with the body at all. +- Returning a redirect response; in this case, you likely just want to specify the target for the + `Location` header, and optionally the status code. + +Starting with version 1.1, Diactoros offers several custom response types for simplifying these +common tasks. + +## Text Responses + +`Laminas\Diactoros\Response\TextResponse` creates a plain text response. It sets the +`Content-Type` header to `text/plain` by default: + +```php +$response = new Laminas\Diactoros\Response\TextResponse('Hello world!'); +``` + +The constructor accepts two additional arguments: a status code and an array of headers. + +```php +$response = new Laminas\Diactoros\Response\TextResponse( + $text, + 200, + ['Content-Type' => ['text/csv']] +); +``` + +## HTML Responses + +`Laminas\Diactoros\Response\HtmlResponse` allows specifying HTML as a payload, and sets the +`Content-Type` header to `text/html` by default: + +```php +$response = new Laminas\Diactoros\Response\HtmlResponse($htmlContent); +``` + +The constructor allows passing two additional arguments: a status code, and an array of headers. +These allow you to further seed the initial state of the response, as well as to override the +`Content-Type` header if desired: + +```php +$response = new Laminas\Diactoros\Response\HtmlResponse( + $htmlContent, + 200, + ['Content-Type' => ['application/xhtml+xml']] +); +``` + +Headers must be in the same format as you would provide to the +[Response constructor](api.md#response-message). + +## XML Responses + +`Laminas\Diactoros\Response\XmlResponse` allows specifying XML as a payload, and sets the +`Content-Type` header to `application/xml` by default: + +```php +$response = new Laminas\Diactoros\Response\XmlResponse($xml); +``` + +The constructor allows passing two additional arguments: a status code, and an array of headers. +These allow you to further seed the initial state of the response, as well as to override the +`Content-Type` header if desired: + +```php +$response = new Laminas\Diactoros\Response\XmlResponse( + $xml, + 200, + ['Content-Type' => ['application/hal+xml']] +); +``` + +Headers must be in the same format as you would provide to the +[Response constructor](api.md#response-message). + +## JSON Responses + +`Laminas\Diactoros\Response\JsonResponse` accepts a data structure to convert to JSON, and sets +the `Content-Type` header to `application/json`: + +```php +$response = new Laminas\Diactoros\Response\JsonResponse($data); +``` + +If providing an object, we recommend implementing [JsonSerializable](http://php.net/JsonSerializable) +to ensure your object is correctly serialized. + +Just like the `HtmlResponse`, the `JsonResponse` allows passing two additional arguments — a +status code, and an array of headers — to allow you to further seed the initial state of the +response: + +```php +$response = new Laminas\Diactoros\Response\JsonResponse( + $data, + 200, + ['Content-Type' => ['application/hal+json']] +); +``` + +Finally, `JsonResponse` allows a fourth optional argument, the flags to provide to `json_encode()`. +By default, these are set to `JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT` (integer +15), providing [RFC 4627](http://tools.ietf.org/html/rfc4627) compliant JSON capable of embedding in +HTML. If you want to specify a different set of flags, use the fourth constructor argument: + +```php +$response = new Laminas\Diactoros\Response\JsonResponse( + $data, + 200, + [], + JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT +); +``` + +## Empty Responses + +Many API actions allow returning empty responses: + +- `201 Created` responses are often empty, and only include a `Link` or `Location` header pointing + to the newly created resource. +- `202 Accepted` responses are typically empty, indicating that the new entity has been received, + but not yet processed. +- `204 No Content` responses are, by definition, empty, and often used as a success response when + deleting an entity. + +`Laminas\Diactoros\Response\EmptyResponse` is a `Laminas\Diactoros\Response` extension that, by default, +returns an empty response with a 204 status. Its constructor allows passing the status and headers +only: + +```php +namespace Laminas\Diactoros\Response; + +use Laminas\Diactoros\Response; + +class EmptyResponse extends Response +{ + public function __construct($status = 204, array $headers = []); +} +``` + +An empty, read-only body is injected at instantiation, ensuring no write operations are possible on +the response. Usage is typically one of the following forms: + +```php +use Laminas\Diactoros\Response\EmptyResponse; + +// Basic 204 response: +$response = new EmptyResponse(); + +// 201 response with location header: +$response = new EmptyResponse(201, [ + 'Location' => [ $url ], +]); + +// Alternately, set the header after instantiation: +$response = (new EmptyResponse(201))->withHeader('Location', $url); +``` + +## Redirects + +`Laminas\Diactoros\Response\RedirectResponse` is a `Laminas\Diactoros\Response` extension for producing +redirect responses. The only required argument is a URI, which may be provided as either a string or +`Psr\Http\Message\UriInterface` instance. By default, the status 302 is used, and no other headers +are produced; you may alter these via the additional optional arguments: + +```php +namespace Laminas\Diactoros\Response; + +use Laminas\Diactoros\Response; + +class RedirectResponse extends Response +{ + public function __construct($uri, $status = 302, array $headers = []); +} +``` + +Typical usage is: + +```php +use Laminas\Diactoros\Response\RedirectResponse; + +// 302 redirect: +$response = new RedirectResponse('/user/login'); + +// 301 redirect: +$response = new RedirectResponse('/user/login', 301); + +// using a URI instance (e.g., by altering the request URI instance) +$uri = $request->getUri(); +$response = new RedirectResponse($uri->withPath('/login')); +``` + +## Creating custom Responses + +PHP allows constructor overloading. What this means is that constructors of extending classes can +define completely different argument sets without conflicting with the parent implementation. +Considering that most custom response types do not need to change internal functionality, but +instead focus on user experience (i.e., simplifying instantiation), this fact can be leveraged to +create your custom types. + +The general pattern will be something like this: + +```php +use Laminas\Diactoros\Response; + +class MyCustomResponse extends Response +{ + public function __construct($data, $status = 200, array $headers = []) + { + // - Do something with $data, and create a Stream for the body (if necessary). + // - Maybe set some default headers. + + parent::__construct($body, $status, $headers); + } +} +``` + +Note the call to `parent::__construct()`. This is particularly relevant, as the implementation at +the time of writing has all class properties marked as private, making them inaccessible to +extensions; this is done to protect encapsulation and ensure consistency of operations between +instances. + +If you don't want to go the extension route (perhaps you don't want another `ResponseInterface` +implementation within your object graph) you can instead create a factory. As an example: + +```php +$plainTextResponse = function ($text, $status = 200, array $headers = []) { + $response = new Laminas\Diactoros\Response('php://temp', $status, $headers); + $response->getBody()->write($text); + if (! $response->hasHeader('Content-Type')) { + $response = $response->withHeader('Content-Type', 'text/plain'); + } + return $response; +}; + +$response = $plainTextResponse('Hello, world!'); +``` + +We recommend following the semantic of providing the status and headers as the final two arguments +for any factory or custom response extensions. diff --git a/docs/book/v3/factories.md b/docs/book/v3/factories.md new file mode 100644 index 00000000..9c8a2db6 --- /dev/null +++ b/docs/book/v3/factories.md @@ -0,0 +1,29 @@ +# Factories + +[PSR-17](https://www.php-fig.org/psr/psr-17/) defines factory interfaces for +creating [PSR-7](https://www.php-fig.org/psr/psr-7/) instances. As of version +2.0.0, Diactoros supplies implementations of each as follows: + +- `Laminas\Diactoros\RequestFactory` +- `Laminas\Diactoros\ResponseFactory` +- `Laminas\Diactoros\ServerRequestFactory` +- `Laminas\Diactoros\StreamFactory` +- `Laminas\Diactoros\UploadedFileFactory` +- `Laminas\Diactoros\UriFactory` + +The `ServerRequestFactory` continues to define the static method +`fromGlobals()`, but also serves as a PSR-17 implementation. + +These classes may be used as described in the specification document for the +purpose of creating Diactoros instances that fulfill PSR-7 typehints. + +## Autoregistration of factories + +- Since 2.3.0 + +When installing Diactoros in a Laminas or Mezzio application, or any application +using the [laminas-component-installer plugin](https://docs.laminas.dev/laminas-component-installer), +you will now be prompted to install its `ConfigProvider` and/or `Module`. When +you do, it registers the Diactoros factory implementations under the PSR-17 +interface names, allowing you to compose instances of the interface in your +application classes.. diff --git a/docs/book/v3/forward-migration.md b/docs/book/v3/forward-migration.md new file mode 100644 index 00000000..375ad512 --- /dev/null +++ b/docs/book/v3/forward-migration.md @@ -0,0 +1,20 @@ +# Preparing for Version 3 + +## ServerRequestFilterInterface defaults + +Introduced in version 2.11.1, the `Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface` is used by `ServerRequestFactory::fromGlobals()` to allow modifying the generated `ServerRequest` instance prior to returning it. +The primary use case is to allow modifying the generated URI based on the presence of headers such as `X-Forwarded-Host`. +When operating behind a reverse proxy, the `Host` header is often rewritten to the name of the node to which the request is being forwarded, and an `X-Forwarded-Host` header is generated with the original `Host` value to allow the server to determine the original host the request was intended for. +(We have always examined the `X-Forwarded-Proto` header; as of 2.11.1, we also examine the `X-Forwarded-Port` header.) + +To accommodate this use case, we created `Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders`. + +Due to potential security issues, it is generally best to only accept these headers if you trust the reverse proxy that has initiated the request. +(This value is found in `$_SERVER['REMOTE_ADDR']`, which is present as `$request->getServerParams()['REMOTE_ADDR']` within PSR-7 implementations.) +`FilterUsingXForwardedHeaders` provides named constructors to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. +To prevent backwards compatibility breaks, we use this filter by default, marked to trust **only proxies on private subnets**. + +Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface` instance, and we recommend explicitly configuring this to utilize the `FilterUsingXForwardedHeaders` if you depend on this functionality. +If you **do not** need the functionality, we recommend specifying `Laminas\Diactoros\ServerRequestFilter\DoNotFilter` as the configured `FilterServerRequestInterface` in your application immediately. + +We will update this documentation with a link to the related functionality in mezzio/mezzio when it is published. diff --git a/docs/book/v3/install.md b/docs/book/v3/install.md new file mode 100644 index 00000000..ff23e9ef --- /dev/null +++ b/docs/book/v3/install.md @@ -0,0 +1,15 @@ +# Installation + +Install this library using composer: + +```bash +$ composer require laminas/laminas-diactoros +``` + +`laminas-diactoros` has the following dependencies (which are managed by Composer): + +- `psr/http-message`, which defines interfaces for HTTP messages, including requests and responses. + `laminas-diactoros` provides implementations of each of these. + +- `psr/http-factory`, which defines interfaces for HTTP message factories. + `laminas-diactoros` provides implementations of each of these. diff --git a/docs/book/v3/migration.md b/docs/book/v3/migration.md new file mode 100644 index 00000000..f54c3077 --- /dev/null +++ b/docs/book/v3/migration.md @@ -0,0 +1,14 @@ +# Migration to Version 3 + +## Removed + +The following features were removed for version 3. + +### GdImage support in `Stream` + +`Laminas\Diactoros\Stream` "supported" usage of resources created via the GD extension. +However, this support was unstable, and largely did not work. +With the update in PHP 8.0 to usage of opaque resource types for all GD resources, it did not work at all. +As such, we have removed the feature entirely. + +If you need to stream an image, the recommendation is to use the functionality in the GD extension to write the image to a temporary file (e.g., `php://temp`), and then to pass that to `Laminas\Diactoros\Stream`. diff --git a/docs/book/v3/overview.md b/docs/book/v3/overview.md new file mode 100644 index 00000000..a3aebd9e --- /dev/null +++ b/docs/book/v3/overview.md @@ -0,0 +1,5 @@ +# Overview + +`laminas-diactoros` is a PHP package containing implementations of the +[PSR-7 HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), +as well as the [PSR-17 HTTP message factory interfaces](https://www.php-fig.org/psr/psr-17/). diff --git a/docs/book/v3/serialization.md b/docs/book/v3/serialization.md new file mode 100644 index 00000000..39a3fa42 --- /dev/null +++ b/docs/book/v3/serialization.md @@ -0,0 +1,59 @@ +# Serialization + +## String + +At times, it's useful to either create a string representation of a message (serialization), or to +cast a string or stream message to an object (deserialization). This package provides features for +this in `Laminas\Diactoros\Request\Serializer` and `Laminas\Diactoros\Response\Serializer`; each provides +the following static methods: + +- `fromString($message)` will create either a `Request` or `Response` instance (based on the + serializer used) from the string message. +- `fromStream(Psr\Http\Message\StreamInterface $stream)` will create either a `Request` or + `Response` instance (based on the serializer used) from the provided stream. +- `toString(Psr\Http\Message\RequestInterface|Psr\Http\Message\ResponseInterface $message)` will + create either a string from the provided message. + +The deserialization methods (`from*()`) will raise exceptions if errors occur while parsing the +message. The serialization methods (`toString()`) will raise exceptions if required data for +serialization is not present in the message instance. + +## Array + +This package also provides features for array serialization using +`Laminas\Diactoros\Request\ArraySerializer` and `Laminas\Diactoros\Response\ArraySerializer`; each provides +the following static methods: + +- `fromArray(array $message)` will create either a `Request` or `Response` instance (based on the + serializer used) from the array message. +- `toArray(Psr\Http\Message\RequestInterface|Psr\Http\Message\ResponseInterface $message)` will + create an array from the provided message. + +The deserialization methods (`fromArray()`) will raise exceptions if errors occur while parsing the +message. + +### Example Usage + +Array serialization can be usesful for log messages: + +```php +class LoggerMiddleware +{ + /** + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) + { + $response = $next($request, $response); + + $this->logger->debug('Request/Response', [ + 'request' => \Laminas\Diactoros\Request\ArraySerializer::toArray($request), + 'response' => \Laminas\Diactoros\Response\ArraySerializer::toArray($response), + ]); + + return $response; + } +} +``` diff --git a/docs/book/v3/server-request-filters.md b/docs/book/v3/server-request-filters.md new file mode 100644 index 00000000..71d6a080 --- /dev/null +++ b/docs/book/v3/server-request-filters.md @@ -0,0 +1,112 @@ +# Server Request Filters + +INFO: **New Feature** +Available since version 2.11.1 + +Server request filters allow you to modify the initial state of a generated `ServerRequest` instance as returned from `Laminas\Diactoros\ServerRequestFactory::fromGlobals()`. +Common use cases include: + +- Generating and injecting a request ID. +- Modifying the request URI based on headers provided (e.g., based on the `X-Forwarded-Host` or `X-Forwarded-Proto` headers). + +## FilterServerRequestInterface + +A request filter implements `Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface`: + +```php +namespace Laminas\Diactoros\ServerRequestFilter; + +use Psr\Http\Message\ServerRequestInterface; + +interface FilterServerRequestInterface +{ + public function __invoke(ServerRequestInterface $request): ServerRequestInterface; +} +``` + +## Implementations + +We provide the following implementations: + +- `DoNotFilter`: returns the provided `$request` verbatim. +- `FilterUsingXForwardedHeaders`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instance that reflects those headers. + +### DoNotFilter + +This filter returns the `$request` argument back verbatim when invoked. + +### FilterUsingXForwardedHeaders + +Servers behind a reverse proxy need mechanisms to determine the original URL requested. +As such, reverse proxies have provided a number of mechanisms for delivering this information, with the use of `X-Forwarded-*` headers being the most prevalant. +These include: + +- `X-Forwarded-Host`: the original `Host` header value. +- `X-Forwarded-Port`: the original port included in the `Host` header value. +- `X-Forwarded-Proto`: the original URI scheme used to make the request (e.g., "http" or "https"). + +`Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders` provides named constructors for choosing whether to never trust proxies, always trust proxies, or choose wich proxies and/or headers to trust in order to modify the URI composed in the request instance to match the original request. +These named constructors are: + +- `FilterUsingXForwardedHeadersFactory::trustProxies(string[] $proxyCIDRList, string[] $trustedHeaders = FilterUsingXForwardedHeaders::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. + Proxies may be specified by IP address, or using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) for subnets; both IPv4 and IPv6 are accepted. + The special string "*" will be translated to two entries, `0.0.0.0/0` and `::/0`. +- `FilterUsingXForwardedHeaders::trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. + It is functionally equivalent to `FilterUsingXForwardedHeaders::trustProxies(['*'])`. +- `FilterUsingXForwardedHeaders::trustReservedSubnets(): void`: when this method is called, the filter will trust requests made from reserved, private subnets. + It is functionally equivalent to `FilterUsingXForwardedHeaders::trustProxies()` with the following elements in the `$proxyCIDRList`: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - ::1/128 (IPv6 localhost) + - fc00::/7 (IPv6 private networks) + - fe80::/10 (IPv6 local-link addresses) + +Internally, the filter checks the `REMOTE_ADDR` server parameter (as retrieved from `getServerParams()`) and compares it against each proxy listed; the first to match indicates trust. + +#### Constants + +The `FilterUsingXForwardedHeaders` defines the following constants for use in specifying various headers: + +- `HEADER_HOST`: corresponds to `X-Forwarded-Host`. +- `HEADER_PORT`: corresponds to `X-Forwarded-Port`. +- `HEADER_PROTO`: corresponds to `X-Forwarded-Proto`. + +#### Example usage + +Trusting all `X-Forwarded-*` headers from any source: + +```php +$filter = FilterUsingXForwardedHeaders::trustAny(); +``` + +Trusting only the `X-Forwarded-Host` header from any source: + +```php +$filter = FilterUsingXForwardedHeaders::trustProxies('0.0.0.0/0', [FilterUsingXForwardedHeaders::HEADER_HOST]); +``` + +Trusting the `X-Forwarded-Host` and `X-Forwarded-Proto` headers from a single Class C subnet: + +```php +$filter = FilterUsingXForwardedHeaders::trustProxies( + '192.168.1.0/24', + [FilterUsingXForwardedHeaders::HEADER_HOST, FilterUsingXForwardedHeaders::HEADER_PROTO] +); +``` + +Trusting the `X-Forwarded-Host` header from either a Class A or a Class C subnet: + +```php +$filter = FilterUsingXForwardedHeaders::trustProxies( + ['10.1.1.0/16', '192.168.1.0/24'], + [FilterUsingXForwardedHeaders::HEADER_HOST, FilterUsingXForwardedHeaders::HEADER_PROTO] +); +``` + +Trusting any `X-Forwarded-*` header from any private subnet: + +```php +$filter = FilterUsingXForwardedHeaders::trustReservedSubnets(); +``` diff --git a/docs/book/v3/usage.md b/docs/book/v3/usage.md new file mode 100644 index 00000000..45939181 --- /dev/null +++ b/docs/book/v3/usage.md @@ -0,0 +1,138 @@ +# Usage + +Usage will differ based on whether you are writing an HTTP client, or a server-side application. + +For HTTP client purposes, you will create and populate a `Request` instance, and the client should +return a `Response` instance. + +For server-side applications, you will create a `ServerRequest` instance, and populate and return a +`Response` instance. + +## HTTP Clients + +A client will _send_ a request, and _return_ a response. As a developer, you will _create_ and +_populate_ the request, and then _introspect_ the response. Both requests and responses are +immutable; if you make changes — e.g., by calling setter methods — you must capture the return +value, as it is a new instance. + +```php +// Create a request +$request = (new Laminas\Diactoros\Request()) + ->withUri(new Laminas\Diactoros\Uri('http://example.com')) + ->withMethod('PATCH') + ->withAddedHeader('Authorization', 'Bearer ' . $token) + ->withAddedHeader('Content-Type', 'application/json'); + +// OR: +$request = new Laminas\Diactoros\Request( + 'http://example.com', + 'PATCH', + 'php://memory', + [ + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/json', + ] +); + +// If you want to set a non-origin-form request target, set the +// request-target explicitly: +$request = $request->withRequestTarget((string) $uri); // absolute-form +$request = $request->withRequestTarget($uri->getAuthority()); // authority-form +$request = $request->withRequestTarget('*'); // asterisk-form + +// Once you have the instance: +$request->getBody()->write(json_encode($data)); +$response = $client->send($request); + +printf("Response status: %d (%s)\n", $response->getStatusCode(), $response->getReasonPhrase()); +printf("Headers:\n"); +foreach ($response->getHeaders() as $header => $values) { + printf(" %s: %s\n", $header, implode(', ', $values)); +} +printf("Message:\n%s\n", $response->getBody()); +``` + +(Note: `laminas-diactoros` does NOT ship with a client implementation; the above is just an +illustration of a possible implementation.) + +## Server-Side Applications + +Server-side applications will need to marshal the incoming request based on superglobals, and will +then populate and send a response. + +### Marshaling an incoming Request + +PHP contains a plethora of information about the incoming request, and keeps that information in a +variety of locations. `Laminas\Diactoros\ServerRequestFactory::fromGlobals()` can simplify marshaling +that information into a request instance. + +You can call the factory method with or without the following arguments, in the following order: + +- `$server`, typically `$_SERVER` +- `$query`, typically `$_GET` +- `$body`, typically `$_POST` +- `$cookies`, typically `$_COOKIE` +- `$files`, typically `$_FILES` + +The method will then return a `Laminas\Diactoros\ServerRequest` instance. If any argument is omitted, +the associated superglobal will be used. + +```php +$request = Laminas\Diactoros\ServerRequestFactory::fromGlobals( + $_SERVER, + $_GET, + $_POST, + $_COOKIE, + $_FILES +); +``` + +When no cookie array is supplied, `fromGlobals` will first try to parse the supplied `cookie` header +before falling back to the `$_COOKIE` superglobal. This is done because PHP has some legacy handling +for request parameters which were then registered as global variables. Due to this, cookies with a period +in the name were renamed with underlines. By getting the cookies directly from the cookie header, you have +access to the original cookies in the way you set them in your application and they are send by the user +agent. + +> #### Strict Content- header matching +> +> Available since version 2.6.0 +> +> By default, Diactoros will resolve any `$_SERVER` keys matching the prefix `CONTENT_` as HTTP headers. +> However, the proper behavior is to only match `CONTENT_TYPE`, `CONTENT_LENGTH`, and `CONTENT_MD5`, mapping them to `Content-Type`, `Content-Length`, and `Content-MD5` headers, respectively. +> Since changing the existing behavior may break some applications, we will not make the functionality more restrictive before version 3.0.0. +> If you are running into issues whereby you have ENV variables that are being munged into request headers, you can define the following ENV variable in your application to enable the more strict behavior: +> +> - LAMINAS_DIACTOROS_STRICT_CONTENT_HEADER_LOOKUP +> +> As an example, you could define it in your application's `.env` file if you are using [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv): +> +> ```env +> LAMINAS_DIACTOROS_STRICT_CONTENT_HEADER_LOOKUP=true +> ``` +> +> Alternately, you could define it as a php-fpm or Apache environment variable. +> +> Once this ENV variable is present, the logic for identifying `Content-*` headers will only look at the `CONTENT_TYPE`, `CONTENT_LENGTH`, and `CONTENT_MD5` variables in `$_SERVER`, and skip over any others. + +### Manipulating the Response + +Use the response object to add headers and provide content for the response. Writing to the body +does not create a state change in the response, so it can be done without capturing the return +value. Manipulating headers does, however. + +```php +$response = new Laminas\Diactoros\Response(); + +// Write to the response body: +$response->getBody()->write("some content\n"); + +// Multiple calls to write() append: +$response->getBody()->write("more content\n"); // now "some content\nmore content\n" + +// Add headers +// Note: headers do not need to be added before data is written to the body! +$response = $response + ->withHeader('Content-Type', 'text/plain') + ->withAddedHeader('X-Show-Something', 'something'); +``` diff --git a/mkdocs.yml b/mkdocs.yml index 7d41499c..5143f1bd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,12 +2,25 @@ docs_dir: docs/book site_dir: docs/html extra: project: Components - current_version: v2 + current_version: v3 versions: + - v3 - v2 - v1 nav: - Home: index.md + - v3: + - Overview: v3/overview.md + - Installation: v3/install.md + - Usage: v3/usage.md + - Reference: + - Factories: v3/factories.md + - "Server Request Filters": v3/server-request-filters.md + - "Custom Responses": v3/custom-responses.md + - Serialization: v3/serialization.md + - API: v3/api.md + - Migration: + - "Migration to Version 3": v3/migration.md - v2: - Overview: v2/overview.md - Installation: v2/install.md @@ -37,10 +50,10 @@ plugins: - search - redirects: redirect_maps: - overview.md: v2/overview.md - install.md: v2/install.md - usage.md: v2/usage.md - custom-responses.md: v2/custom-responses.md + overview.md: v3/overview.md + install.md: v3/install.md + usage.md: v3/usage.md + custom-responses.md: v3/custom-responses.md emitting-responses.md: v1/emitting-responses.md - serialization.md: v2/serialization.md - api.md: v2/api.md + serialization.md: v3/serialization.md + api.md: v3/api.md diff --git a/src/Stream.php b/src/Stream.php index c38590fa..2af1bb8b 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -4,7 +4,6 @@ namespace Laminas\Diactoros; -use GdImage; use Psr\Http\Message\StreamInterface; use RuntimeException; use Stringable; @@ -39,7 +38,7 @@ class Stream implements StreamInterface, Stringable /** * A list of allowed stream resource types that are allowed to instantiate a Stream */ - private const ALLOWED_STREAM_RESOURCE_TYPES = ['gd', 'stream']; + private const ALLOWED_STREAM_RESOURCE_TYPES = ['stream']; /** @var resource|null */ protected $resource; @@ -358,10 +357,6 @@ private function isValidStreamResourceType(mixed $resource): bool return in_array(get_resource_type($resource), self::ALLOWED_STREAM_RESOURCE_TYPES, true); } - if ($resource instanceof GdImage) { - return true; - } - return false; } } diff --git a/test/StreamTest.php b/test/StreamTest.php index 245d7bde..2280004c 100644 --- a/test/StreamTest.php +++ b/test/StreamTest.php @@ -7,6 +7,7 @@ use CurlHandle; use GdImage; use InvalidArgumentException; +use Laminas\Diactoros\Exception\InvalidArgumentException as DiactorosInvalidArgumentException; use Laminas\Diactoros\Stream; use PHPUnit\Framework\TestCase; use ReflectionProperty; @@ -67,12 +68,13 @@ public function testCanInstantiateWithStreamResource(): void $this->assertInstanceOf(Stream::class, $stream); } - public function testCanInstantiateWithGDResource(): void + public function testCannotInstantiateWithGDResource(): void { $resource = imagecreate(1, 1); self::assertInstanceOf(GdImage::class, $resource); - $stream = new Stream($resource); - $this->assertInstanceOf(Stream::class, $stream); + + $this->expectException(DiactorosInvalidArgumentException::class); + new Stream($resource); } public function testIsReadableReturnsFalseIfStreamIsNotReadable(): void