[BUGFIX] Re-introduce WebDAV-related HTTP methods
[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 // WebDAV methods
62 'COPY',
63 'LOCK',
64 'MKCOL',
65 'MOVE',
66 'PROPFIND',
67 'PROPPATCH',
68 'UNLOCK'
69 );
70
71 /**
72 * An instance of the Uri object
73 * @var UriInterface
74 */
75 protected $uri;
76
77 /**
78 * Constructor, the only place to set all parameters of this Request
79 *
80 * @param NULL|string $uri URI for the request, if any.
81 * @param NULL|string $method HTTP method for the request, if any.
82 * @param string|resource|StreamInterface $body Message body, if any.
83 * @param array $headers Headers for the message, if any.
84 * @throws \InvalidArgumentException for any invalid value.
85 */
86 public function __construct($uri = NULL, $method = NULL, $body = 'php://input', array $headers = array()) {
87
88 // Build a streamable object for the body
89 if (!is_string($body) && !is_resource($body) && !$body instanceof StreamInterface) {
90 throw new \InvalidArgumentException('Body must be a string stream resource identifier, a stream resource, or a StreamInterface instance', 1436717271);
91 }
92
93 if (!$body instanceof StreamInterface) {
94 $body = new Stream($body);
95 }
96
97 if (is_string($uri)) {
98 $uri = new Uri($uri);
99 }
100
101 if (!$uri instanceof UriInterface && $uri !== NULL) {
102 throw new \InvalidArgumentException('Invalid URI provided; must be null, a string, or a UriInterface instance', 1436717272);
103 }
104
105 $this->validateMethod($method);
106
107 $this->method = $method;
108 $this->uri = $uri;
109 $this->body = $body;
110 list($this->headerNames, $headers) = $this->filterHeaders($headers);
111 $this->assertHeaders($headers);
112 $this->headers = $headers;
113 }
114
115 /**
116 * Retrieves all message header values.
117 *
118 * The keys represent the header name as it will be sent over the wire, and
119 * each value is an array of strings associated with the header.
120 *
121 * // Represent the headers as a string
122 * foreach ($message->getHeaders() as $name => $values) {
123 * echo $name . ": " . implode(", ", $values);
124 * }
125 *
126 * // Emit headers iteratively:
127 * foreach ($message->getHeaders() as $name => $values) {
128 * foreach ($values as $value) {
129 * header(sprintf('%s: %s', $name, $value), false);
130 * }
131 * }
132 *
133 * While header names are not case-sensitive, getHeaders() will preserve the
134 * exact case in which headers were originally specified.
135 *
136 * @return array Returns an associative array of the message's headers. Each
137 * key MUST be a header name, and each value MUST be an array of strings
138 * for that header.
139 */
140 public function getHeaders() {
141 $headers = parent::getHeaders();
142 if (!$this->hasHeader('host') && ($this->uri && $this->uri->getHost())) {
143 $headers['host'] = [$this->getHostFromUri()];
144 }
145 return $headers;
146 }
147
148 /**
149 * Retrieves a message header value by the given case-insensitive name.
150 *
151 * This method returns an array of all the header values of the given
152 * case-insensitive header name.
153 *
154 * If the header does not appear in the message, this method MUST return an
155 * empty array.
156 *
157 * @param string $name Case-insensitive header field name.
158 * @return string[] An array of string values as provided for the given
159 * header. If the header does not appear in the message, this method MUST
160 * return an empty array.
161 */
162 public function getHeader($header) {
163 if (!$this->hasHeader($header) && strtolower($header) === 'host' && ($this->uri && $this->uri->getHost())) {
164 return array($this->getHostFromUri());
165 }
166 return parent::getHeader($header);
167 }
168
169 /**
170 * Retrieve the host from the URI instance
171 *
172 * @return string
173 */
174 protected function getHostFromUri() {
175 $host = $this->uri->getHost();
176 $host .= $this->uri->getPort() ? ':' . $this->uri->getPort() : '';
177 return $host;
178 }
179
180 /**
181 * Retrieves the message's request target.
182 *
183 * Retrieves the message's request-target either as it will appear (for
184 * clients), as it appeared at request (for servers), or as it was
185 * specified for the instance (see withRequestTarget()).
186 *
187 * In most cases, this will be the origin-form of the composed URI,
188 * unless a value was provided to the concrete implementation (see
189 * withRequestTarget() below).
190 *
191 * If no URI is available, and no request-target has been specifically
192 * provided, this method MUST return the string "/".
193 *
194 * @return string
195 */
196 public function getRequestTarget() {
197 if ($this->requestTarget !== NULL) {
198 return $this->requestTarget;
199 }
200 if (!$this->uri) {
201 return '/';
202 }
203 $target = $this->uri->getPath();
204
205 if ($this->uri->getQuery()) {
206 $target .= '?' . $this->uri->getQuery();
207 }
208
209 if (empty($target)) {
210 $target = '/';
211 }
212 return $target;
213 }
214
215 /**
216 * Return an instance with the specific request-target.
217 *
218 * If the request needs a non-origin-form request-target — e.g., for
219 * specifying an absolute-form, authority-form, or asterisk-form —
220 * this method may be used to create an instance with the specified
221 * request-target, verbatim.
222 *
223 * This method MUST be implemented in such a way as to retain the
224 * immutability of the message, and MUST return an instance that has the
225 * changed request target.
226 *
227 * @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
228 * request-target forms allowed in request messages)
229 *
230 * @param mixed $requestTarget
231 * @return Request
232 */
233 public function withRequestTarget($requestTarget) {
234 if (preg_match('#\s#', $requestTarget)) {
235 throw new \InvalidArgumentException('Invalid request target provided which contains whitespaces.', 1436717273);
236 }
237 $clonedObject = clone $this;
238 $clonedObject->requestTarget = $requestTarget;
239 return $clonedObject;
240 }
241
242 /**
243 * Retrieves the HTTP method of the request, defaults to GET
244 *
245 * @return string Returns the request method.
246 */
247 public function getMethod() {
248 return !empty($this->method) ? $this->method : 'GET';
249 }
250
251 /**
252 * Return an instance with the provided HTTP method.
253 *
254 * While HTTP method names are typically all uppercase characters, HTTP
255 * method names are case-sensitive and thus implementations SHOULD NOT
256 * modify the given string.
257 *
258 * This method MUST be implemented in such a way as to retain the
259 * immutability of the message, and MUST return an instance that has the
260 * changed request method.
261 *
262 * @param string $method Case-sensitive method.
263 * @return Request
264 * @throws \InvalidArgumentException for invalid HTTP methods.
265 */
266 public function withMethod($method) {
267 $clonedObject = clone $this;
268 $clonedObject->method = $method;
269 return $clonedObject;
270 }
271
272 /**
273 * Retrieves the URI instance.
274 *
275 * This method MUST return a UriInterface instance.
276 *
277 * @link http://tools.ietf.org/html/rfc3986#section-4.3
278 * @return \Psr\Http\Message\UriInterface Returns a UriInterface instance
279 * representing the URI of the request.
280 */
281 public function getUri() {
282 return $this->uri;
283 }
284
285 /**
286 * Returns an instance with the provided URI.
287 *
288 * This method MUST update the Host header of the returned request by
289 * default if the URI contains a host component. If the URI does not
290 * contain a host component, any pre-existing Host header MUST be carried
291 * over to the returned request.
292 *
293 * You can opt-in to preserving the original state of the Host header by
294 * setting `$preserveHost` to `true`. When `$preserveHost` is set to
295 * `true`, this method interacts with the Host header in the following ways:
296 *
297 * - If the the Host header is missing or empty, and the new URI contains
298 * a host component, this method MUST update the Host header in the returned
299 * request.
300 * - If the Host header is missing or empty, and the new URI does not contain a
301 * host component, this method MUST NOT update the Host header in the returned
302 * request.
303 * - If a Host header is present and non-empty, this method MUST NOT update
304 * the Host header in the returned request.
305 *
306 * This method MUST be implemented in such a way as to retain the
307 * immutability of the message, and MUST return an instance that has the
308 * new UriInterface instance.
309 *
310 * @link http://tools.ietf.org/html/rfc3986#section-4.3
311 *
312 * @param \Psr\Http\Message\UriInterface $uri New request URI to use.
313 * @param bool $preserveHost Preserve the original state of the Host header.
314 * @return Request
315 */
316 public function withUri(UriInterface $uri, $preserveHost = FALSE) {
317 $clonedObject = clone $this;
318 $clonedObject->uri = $uri;
319
320 if ($preserveHost) {
321 return $clonedObject;
322 }
323
324 if (!$uri->getHost()) {
325 return $clonedObject;
326 }
327
328 $host = $uri->getHost();
329
330 if ($uri->getPort()) {
331 $host .= ':' . $uri->getPort();
332 }
333
334 $clonedObject->headerNames['host'] = 'Host';
335 $clonedObject->headers['Host'] = array($host);
336 return $clonedObject;
337 }
338
339 /**
340 * Validate the HTTP method, helper function.
341 *
342 * @param NULL|string $method
343 * @throws \InvalidArgumentException on invalid HTTP method.
344 */
345 protected function validateMethod($method) {
346 if ($method !== NULL) {
347 if (!is_string($method)) {
348 $methodAsString = is_object($method) ? get_class($method) : gettype($method);
349 throw new \InvalidArgumentException('Unsupported HTTP method "' . $methodAsString . '".', 1436717274);
350 }
351 $method = strtoupper($method);
352 if (!in_array($method, $this->supportedMethods, TRUE)) {
353 throw new \InvalidArgumentException('Unsupported HTTP method "' . $method. '".', 1436717275);
354 }
355 }
356 }
357 }