1<?php
2
3/*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16namespace TYPO3\CMS\Extbase\Mvc;
17
18use TYPO3\CMS\Core\Page\PageRenderer;
19use TYPO3\CMS\Core\Utility\GeneralUtility;
20use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
21
22/**
23 * A generic and very basic response implementation
24 */
25class Response implements ResponseInterface
26{
27    /**
28     * @var string The response content
29     */
30    protected $content;
31
32    /**
33     * The HTTP headers which will be sent in the response
34     *
35     * @var array
36     */
37    protected $headers = [];
38
39    /**
40     * Additional header tags
41     *
42     * @var array
43     */
44    protected $additionalHeaderData = [];
45
46    /**
47     * The HTTP status code
48     *
49     * @var int
50     */
51    protected $statusCode;
52
53    /**
54     * The HTTP status message
55     *
56     * @var string
57     */
58    protected $statusMessage = 'OK';
59
60    /**
61     * The Request which generated the Response
62     *
63     * @var \TYPO3\CMS\Extbase\Mvc\Request
64     */
65    protected $request;
66
67    /**
68     * The standardized and other important HTTP Status messages
69     *
70     * @var array
71     */
72    protected $statusMessages = [
73        // INFORMATIONAL CODES
74        100 => 'Continue',
75        101 => 'Switching Protocols',
76        102 => 'Processing',
77        103 => 'Early Hints',
78        // SUCCESS CODES
79        200 => 'OK',
80        201 => 'Created',
81        202 => 'Accepted',
82        203 => 'Non-Authoritative Information',
83        204 => 'No Content',
84        205 => 'Reset Content',
85        206 => 'Partial Content',
86        207 => 'Multi-status',
87        208 => 'Already Reported',
88        226 => 'IM Used',
89        // REDIRECTION CODES
90        300 => 'Multiple Choices',
91        301 => 'Moved Permanently',
92        302 => 'Found',
93        303 => 'See Other',
94        304 => 'Not Modified',
95        305 => 'Use Proxy',
96        306 => 'Switch Proxy', // Deprecated
97        307 => 'Temporary Redirect',
98        308 => 'Permanent Redirect',
99        // CLIENT ERROR
100        400 => 'Bad Request',
101        401 => 'Unauthorized',
102        402 => 'Payment Required',
103        403 => 'Forbidden',
104        404 => 'Not Found',
105        405 => 'Method Not Allowed',
106        406 => 'Not Acceptable',
107        407 => 'Proxy Authentication Required',
108        408 => 'Request Timeout',
109        409 => 'Conflict',
110        410 => 'Gone',
111        411 => 'Length Required',
112        412 => 'Precondition Failed',
113        413 => 'Request Entity Too Large',
114        414 => 'URI Too Long',
115        415 => 'Unsupported Media Type',
116        416 => 'Requested range not satisfiable',
117        417 => 'Expectation Failed',
118        418 => 'I\'m a teapot',
119        422 => 'Unprocessable Entity',
120        423 => 'Locked',
121        424 => 'Failed Dependency',
122        425 => 'Unordered Collection',
123        426 => 'Upgrade Required',
124        428 => 'Precondition Required',
125        429 => 'Too Many Requests',
126        431 => 'Request Header Fields Too Large',
127        451 => 'Unavailable For Legal Reasons',
128        // SERVER ERROR
129        500 => 'Internal Server Error',
130        501 => 'Not Implemented',
131        502 => 'Bad Gateway',
132        503 => 'Service Unavailable',
133        504 => 'Gateway Time-out',
134        505 => 'HTTP Version not supported',
135        506 => 'Variant Also Negotiates',
136        507 => 'Insufficient Storage',
137        508 => 'Loop Detected',
138        509 => 'Bandwidth Limit Exceeded',
139        511 => 'Network Authentication Required',
140    ];
141
142    /**
143     * Overrides and sets the content of the response
144     *
145     * @param string $content The response content
146     */
147    public function setContent($content)
148    {
149        $this->content = $content;
150    }
151
152    /**
153     * Appends content to the already existing content.
154     *
155     * @param string $content More response content
156     */
157    public function appendContent($content)
158    {
159        $this->content .= $content;
160    }
161
162    /**
163     * Returns the response content without sending it.
164     *
165     * @return string The response content
166     */
167    public function getContent()
168    {
169        return $this->content;
170    }
171
172    /**
173     * Fetches the content, returns and clears it.
174     *
175     * @return string
176     * @internal only to be used within Extbase, not part of TYPO3 Core API.
177     */
178    public function shutdown()
179    {
180        $content = $this->getContent();
181        $this->setContent('');
182        return $content;
183    }
184
185    /**
186     * Returns the content of the response.
187     *
188     * @return string
189     */
190    public function __toString()
191    {
192        return $this->getContent();
193    }
194
195    /**
196     * Sets the HTTP status code and (optionally) a customized message.
197     *
198     * @param int $code The status code
199     * @param string $message If specified, this message is sent instead of the standard message
200     * @throws \InvalidArgumentException if the specified status code is not valid
201     */
202    public function setStatus($code, $message = null)
203    {
204        if (!is_int($code)) {
205            throw new \InvalidArgumentException('The HTTP status code must be of type integer, ' . gettype($code) . ' given.', 1220526013);
206        }
207        if ($message === null && !isset($this->statusMessages[$code])) {
208            throw new \InvalidArgumentException('No message found for HTTP status code "' . $code . '".', 1220526014);
209        }
210        $this->statusCode = $code;
211        $this->statusMessage = $message ?? $this->statusMessages[$code];
212    }
213
214    /**
215     * Returns status code and status message.
216     *
217     * @return string The status code and status message, eg. "404 Not Found
218     */
219    public function getStatus()
220    {
221        return $this->statusCode . ' ' . $this->statusMessage;
222    }
223
224    /**
225     * Returns the status code, if not set, uses the OK status code 200
226     *
227     * @return int
228     * @internal only use for backend module handling
229     */
230    public function getStatusCode()
231    {
232        return $this->statusCode ?: 200;
233    }
234
235    /**
236     * Sets the specified HTTP header
237     *
238     * @param string $name Name of the header, for example "Location", "Content-Description" etc.
239     * @param mixed $value The value of the given header
240     * @param bool $replaceExistingHeader If a header with the same name should be replaced. Default is TRUE.
241     * @throws \InvalidArgumentException
242     */
243    public function setHeader($name, $value, $replaceExistingHeader = true)
244    {
245        if (stripos($name, 'HTTP') === 0) {
246            throw new \InvalidArgumentException('The HTTP status header must be set via setStatus().', 1220541963);
247        }
248        if ($replaceExistingHeader === true || !isset($this->headers[$name])) {
249            $this->headers[$name] = [$value];
250        } else {
251            $this->headers[$name][] = $value;
252        }
253    }
254
255    /**
256     * Returns the HTTP headers - including the status header - of this web response
257     *
258     * @return string[] The HTTP headers
259     */
260    public function getHeaders()
261    {
262        $preparedHeaders = [];
263        if ($this->statusCode !== null) {
264            $protocolVersion = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0';
265            $statusHeader = $protocolVersion . ' ' . $this->statusCode . ' ' . $this->statusMessage;
266            $preparedHeaders[] = $statusHeader;
267        }
268        foreach ($this->headers as $name => $values) {
269            foreach ($values as $value) {
270                $preparedHeaders[] = $name . ': ' . $value;
271            }
272        }
273        return $preparedHeaders;
274    }
275
276    /**
277     * Returns the HTTP headers grouped by name without the status header
278     *
279     * @return array all headers set for this request
280     * @internal only used within TYPO3 Core to convert to PSR-7 response headers
281     */
282    public function getUnpreparedHeaders(): array
283    {
284        return $this->headers;
285    }
286
287    /**
288     * Sends the HTTP headers.
289     *
290     * If headers have already been sent, this method fails silently.
291     */
292    public function sendHeaders()
293    {
294        if (headers_sent() === true) {
295            return;
296        }
297        foreach ($this->getHeaders() as $header) {
298            header($header);
299        }
300    }
301
302    /**
303     * Renders and sends the whole web response
304     */
305    public function send()
306    {
307        $this->sendHeaders();
308        if ($this->content !== null) {
309            echo $this->getContent();
310        }
311    }
312
313    /**
314     * Adds an additional header data (something like
315     * '<script src="myext/Resources/JavaScript/my.js"></script>'
316     * )
317     *
318     * @TODO The workaround and the $request member should be removed again, once the PageRender does support non-cached USER_INTs
319     * @param string $additionalHeaderData The value additional header
320     * @throws \InvalidArgumentException
321     */
322    public function addAdditionalHeaderData($additionalHeaderData)
323    {
324        if (!is_string($additionalHeaderData)) {
325            throw new \InvalidArgumentException('The additional header data must be of type String, ' . gettype($additionalHeaderData) . ' given.', 1237370877);
326        }
327        if ($this->request->isCached()) {
328            /** @var PageRenderer $pageRenderer */
329            $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
330            $pageRenderer->addHeaderData($additionalHeaderData);
331        } else {
332            $this->additionalHeaderData[] = $additionalHeaderData;
333        }
334    }
335
336    /**
337     * Returns the additional header data
338     *
339     * @return array The additional header data
340     */
341    public function getAdditionalHeaderData()
342    {
343        return $this->additionalHeaderData;
344    }
345
346    /**
347     * @param \TYPO3\CMS\Extbase\Mvc\Request $request
348     * @internal only to be used within Extbase, not part of TYPO3 Core API.
349     */
350    public function setRequest(Request $request)
351    {
352        $this->request = $request;
353    }
354
355    /**
356     * @return \TYPO3\CMS\Extbase\Mvc\Request
357     * @internal only to be used within Extbase, not part of TYPO3 Core API.
358     */
359    public function getRequest()
360    {
361        return $this->request;
362    }
363
364    /**
365     * @return TypoScriptFrontendController
366     */
367    protected function getTypoScriptFrontendController()
368    {
369        return $GLOBALS['TSFE'];
370    }
371}
372