[FEATURE] Introduce Request/Response based on PSR-7
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Http / Request.php
1 <?php
2 namespace TYPO3\CMS\Core\Http;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Psr\Http\Message\RequestInterface;
18 use Psr\Http\Message\UriInterface;
19 use Psr\Http\Message\StreamInterface;
20
21 /**
22 * Default implementation for the RequestInterface of the PSR-7 standard
23 * It is the base for any request sent BY PHP.
24 *
25 * Please see ServerRequest for the typical use cases in the framework.
26 *
27 * Highly inspired by https://github.com/phly/http/
28 *
29 * @internal Note that this is not public API yet.
30 */
31 class Request extends Message implements RequestInterface {
32
33 /**
34 * The request-target, if it has been provided or calculated.
35 * @var NULL|string
36 */
37 protected $requestTarget;
38
39 /**
40 * The HTTP method, defaults to GET
41 *
42 * @var string
43 */
44 protected $method;
45
46 /**
47 * Supported HTTP methods
48 *
49 * @var array
50 */
51 protected $supportedMethods = array(
52 'CONNECT',
53 'DELETE',
54 'GET',
55 'HEAD',
56 'OPTIONS',
57 'PATCH',
58 'POST',
59 'PUT',
60 'TRACE'
61 );
62
63 /**
64 * An instance of the Uri object
65 * @var UriInterface
66 */
67 protected $uri;
68
69 /**
70 * Constructor, the only place to set all parameters of this Request
71 *
72 * @param NULL|string $uri URI for the request, if any.
73 * @param NULL|string $method HTTP method for the request, if any.
74 * @param string|resource|StreamInterface $body Message body, if any.
75 * @param array $headers Headers for the message, if any.
76 * @throws \InvalidArgumentException for any invalid value.
77 */
78 public function __construct($uri = NULL, $method = NULL, $body = 'php://input', array $headers = array()) {
79
80 // Build a streamable object for the body
81 if (!is_string($body) && !is_resource($body) && !$body instanceof StreamInterface) {
82 throw new \InvalidArgumentException('Body must be a string stream resource identifier, a stream resource, or a StreamInterface instance', 1436717271);
83 }
84
85 if (!$body instanceof StreamInterface) {
86 $body = new Stream($body);
87 }
88
89 if (is_string($uri)) {
90 $uri = new Uri($uri);
91 }
92
93 if (!$uri instanceof UriInterface && $uri !== NULL) {
94 throw new \InvalidArgumentException('Invalid URI provided; must be null, a string, or a UriInterface instance', 1436717272);
95 }
96
97 $this->validateMethod($method);
98
99 $this->method = $method;
100 $this->uri = $uri;
101 $this->body = $body;
102 list($this->headerNames, $headers) = $this->filterHeaders($headers);
103 $this->assertHeaders($headers);
104 $this->headers = $headers;
105 }
106
107 /**
108 * Retrieves all message header values.
109 *
110 * The keys represent the header name as it will be sent over the wire, and
111 * each value is an array of strings associated with the header.
112 *
113 * // Represent the headers as a string
114 * foreach ($message->getHeaders() as $name => $values) {
115 * echo $name . ": " . implode(", ", $values);
116 * }
117 *
118 * // Emit headers iteratively:
119 * foreach ($message->getHeaders() as $name => $values) {
120 * foreach ($values as $value) {
121 * header(sprintf('%s: %s', $name, $value), false);
122 * }
123 * }
124 *
125 * While header names are not case-sensitive, getHeaders() will preserve the
126 * exact case in which headers were originally specified.
127 *
128 * @return array Returns an associative array of the message's headers. Each
129 * key MUST be a header name, and each value MUST be an array of strings
130 * for that header.
131 */
132 public function getHeaders() {
133 $headers = parent::getHeaders();
134 if (!$this->hasHeader('host') && ($this->uri && $this->uri->getHost())) {
135 $headers['host'] = [$this->getHostFromUri()];
136 }
137 return $headers;
138 }
139
140 /**
141 * Retrieves a message header value by the given case-insensitive name.
142 *
143 * This method returns an array of all the header values of the given
144 * case-insensitive header name.
145 *
146 * If the header does not appear in the message, this method MUST return an
147 * empty array.
148 *
149 * @param string $name Case-insensitive header field name.
150 * @return string[] An array of string values as provided for the given
151 * header. If the header does not appear in the message, this method MUST
152 * return an empty array.
153 */
154 public function getHeader($header) {
155 if (!$this->hasHeader($header) && strtolower($header) === 'host' && ($this->uri && $this->uri->getHost())) {
156 return array($this->getHostFromUri());
157 }
158 return parent::getHeader($header);
159 }
160
161 /**
162 * Retrieve the host from the URI instance
163 *
164 * @return string
165 */
166 protected function getHostFromUri() {
167 $host = $this->uri->getHost();
168 $host .= $this->uri->getPort() ? ':' . $this->uri->getPort() : '';
169 return $host;
170 }
171
172 /**
173 * Retrieves the message's request target.
174 *
175 * Retrieves the message's request-target either as it will appear (for
176 * clients), as it appeared at request (for servers), or as it was
177 * specified for the instance (see withRequestTarget()).
178 *
179 * In most cases, this will be the origin-form of the composed URI,
180 * unless a value was provided to the concrete implementation (see
181 * withRequestTarget() below).
182 *
183 * If no URI is available, and no request-target has been specifically
184 * provided, this method MUST return the string "/".
185 *
186 * @return string
187 */
188 public function getRequestTarget() {
189 if ($this->requestTarget !== NULL) {
190 return $this->requestTarget;
191 }
192 if (!$this->uri) {
193 return '/';
194 }
195 $target = $this->uri->getPath();
196
197 if ($this->uri->getQuery()) {
198 $target .= '?' . $this->uri->getQuery();
199 }
200
201 if (empty($target)) {
202 $target = '/';
203 }
204 return $target;
205 }
206
207 /**
208 * Return an instance with the specific request-target.
209 *
210 * If the request needs a non-origin-form request-target — e.g., for
211 * specifying an absolute-form, authority-form, or asterisk-form —
212 * this method may be used to create an instance with the specified
213 * request-target, verbatim.
214 *
215 * This method MUST be implemented in such a way as to retain the
216 * immutability of the message, and MUST return an instance that has the
217 * changed request target.
218 *
219 * @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
220 * request-target forms allowed in request messages)
221 *
222 * @param mixed $requestTarget
223 * @return Request
224 */
225 public function withRequestTarget($requestTarget) {
226 if (preg_match('#\s#', $requestTarget)) {
227 throw new \InvalidArgumentException('Invalid request target provided which contains whitespaces.', 1436717273);
228 }
229 $clonedObject = clone $this;
230 $clonedObject->requestTarget = $requestTarget;
231 return $clonedObject;
232 }
233
234 /**
235 * Retrieves the HTTP method of the request, defaults to GET
236 *
237 * @return string Returns the request method.
238 */
239 public function getMethod() {
240 return !empty($this->method) ? $this->method : 'GET';
241 }
242
243 /**
244 * Return an instance with the provided HTTP method.
245 *
246 * While HTTP method names are typically all uppercase characters, HTTP
247 * method names are case-sensitive and thus implementations SHOULD NOT
248 * modify the given string.
249 *
250 * This method MUST be implemented in such a way as to retain the
251 * immutability of the message, and MUST return an instance that has the
252 * changed request method.
253 *
254 * @param string $method Case-sensitive method.
255 * @return Request
256 * @throws \InvalidArgumentException for invalid HTTP methods.
257 */
258 public function withMethod($method) {
259 $clonedObject = clone $this;
260 $clonedObject->method = $method;
261 return $clonedObject;
262 }
263
264 /**
265 * Retrieves the URI instance.
266 *
267 * This method MUST return a UriInterface instance.
268 *
269 * @link http://tools.ietf.org/html/rfc3986#section-4.3
270 * @return \Psr\Http\Message\UriInterface Returns a UriInterface instance
271 * representing the URI of the request.
272 */
273 public function getUri() {
274 return $this->uri;
275 }
276
277 /**
278 * Returns an instance with the provided URI.
279 *
280 * This method MUST update the Host header of the returned request by
281 * default if the URI contains a host component. If the URI does not
282 * contain a host component, any pre-existing Host header MUST be carried
283 * over to the returned request.
284 *
285 * You can opt-in to preserving the original state of the Host header by
286 * setting `$preserveHost` to `true`. When `$preserveHost` is set to
287 * `true`, this method interacts with the Host header in the following ways:
288 *
289 * - If the the Host header is missing or empty, and the new URI contains
290 * a host component, this method MUST update the Host header in the returned
291 * request.
292 * - If the Host header is missing or empty, and the new URI does not contain a
293 * host component, this method MUST NOT update the Host header in the returned
294 * request.
295 * - If a Host header is present and non-empty, this method MUST NOT update
296 * the Host header in the returned request.
297 *
298 * This method MUST be implemented in such a way as to retain the
299 * immutability of the message, and MUST return an instance that has the
300 * new UriInterface instance.
301 *
302 * @link http://tools.ietf.org/html/rfc3986#section-4.3
303 *
304 * @param \Psr\Http\Message\UriInterface $uri New request URI to use.
305 * @param bool $preserveHost Preserve the original state of the Host header.
306 * @return Request
307 */
308 public function withUri(UriInterface $uri, $preserveHost = FALSE) {
309 $clonedObject = clone $this;
310 $clonedObject->uri = $uri;
311
312 if ($preserveHost) {
313 return $clonedObject;
314 }
315
316 if (!$uri->getHost()) {
317 return $clonedObject;
318 }
319
320 $host = $uri->getHost();
321
322 if ($uri->getPort()) {
323 $host .= ':' . $uri->getPort();
324 }
325
326 $clonedObject->headerNames['host'] = 'Host';
327 $clonedObject->headers['Host'] = array($host);
328 return $clonedObject;
329 }
330
331 /**
332 * Validate the HTTP method, helper function.
333 *
334 * @param NULL|string $method
335 * @throws \InvalidArgumentException on invalid HTTP method.
336 */
337 protected function validateMethod($method) {
338 if ($method !== NULL) {
339 if (!is_string($method)) {
340 $methodAsString = is_object($method) ? get_class($method) : gettype($method);
341 throw new \InvalidArgumentException('Unsupported HTTP method "' . $methodAsString . '".', 1436717274);
342 }
343 $method = strtoupper($method);
344 if (!in_array($method, $this->supportedMethods, TRUE)) {
345 throw new \InvalidArgumentException('Unsupported HTTP method "' . $method. '".', 1436717275);
346 }
347 }
348 }
349 }