Completed
Push — 4 ( 5fbfd8...bd8494 )
by Ingo
09:20
created

HTTPResponse::__construct()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 12
nop 4
dl 0
loc 13
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Control;
4
5
use InvalidArgumentException;
6
use Monolog\Handler\HandlerInterface;
7
use SilverStripe\Core\Convert;
8
use SilverStripe\Core\Injector\Injectable;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\View\Requirements;
11
12
/**
13
 * Represents a response returned by a controller.
14
 */
15
class HTTPResponse
16
{
17
    use Injectable;
18
19
    /**
20
     * @var array
21
     */
22
    protected static $status_codes = [
23
        100 => 'Continue',
24
        101 => 'Switching Protocols',
25
        200 => 'OK',
26
        201 => 'Created',
27
        202 => 'Accepted',
28
        203 => 'Non-Authoritative Information',
29
        204 => 'No Content',
30
        205 => 'Reset Content',
31
        206 => 'Partial Content',
32
        301 => 'Moved Permanently',
33
        302 => 'Found',
34
        303 => 'See Other',
35
        304 => 'Not Modified',
36
        305 => 'Use Proxy',
37
        307 => 'Temporary Redirect',
38
        308 => 'Permanent Redirect',
39
        400 => 'Bad Request',
40
        401 => 'Unauthorized',
41
        403 => 'Forbidden',
42
        404 => 'Not Found',
43
        405 => 'Method Not Allowed',
44
        406 => 'Not Acceptable',
45
        407 => 'Proxy Authentication Required',
46
        408 => 'Request Timeout',
47
        409 => 'Conflict',
48
        410 => 'Gone',
49
        411 => 'Length Required',
50
        412 => 'Precondition Failed',
51
        413 => 'Request Entity Too Large',
52
        414 => 'Request-URI Too Long',
53
        415 => 'Unsupported Media Type',
54
        416 => 'Request Range Not Satisfiable',
55
        417 => 'Expectation Failed',
56
        422 => 'Unprocessable Entity',
57
        429 => 'Too Many Requests',
58
        500 => 'Internal Server Error',
59
        501 => 'Not Implemented',
60
        502 => 'Bad Gateway',
61
        503 => 'Service Unavailable',
62
        504 => 'Gateway Timeout',
63
        505 => 'HTTP Version Not Supported',
64
    ];
65
66
    /**
67
     * @var array
68
     */
69
    protected static $redirect_codes = [
70
        301,
71
        302,
72
        303,
73
        304,
74
        305,
75
        307,
76
        308,
77
    ];
78
79
    /**
80
     * @var string
81
     */
82
    protected $protocolVersion = '1.0';
83
84
    /**
85
     * @var int
86
     */
87
    protected $statusCode = 200;
88
89
    /**
90
     * @var string
91
     */
92
    protected $statusDescription = "OK";
93
94
    /**
95
     * HTTP Headers like "content-type: text/xml"
96
     *
97
     * @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
98
     * @var array
99
     */
100
    protected $headers = [
101
        "content-type" => "text/html; charset=utf-8",
102
    ];
103
104
    /**
105
     * @var string
106
     */
107
    protected $body = null;
108
109
    /**
110
     * Create a new HTTP response
111
     *
112
     * @param string $body The body of the response
113
     * @param int $statusCode The numeric status code - 200, 404, etc
114
     * @param string $statusDescription The text to be given alongside the status code.
115
     *  See {@link setStatusCode()} for more information.
116
     * @param string $protocolVersion
117
     */
118
    public function __construct($body = null, $statusCode = null, $statusDescription = null, $protocolVersion = null)
119
    {
120
        $this->setBody($body);
121
        if ($statusCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $statusCode of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
122
            $this->setStatusCode($statusCode, $statusDescription);
123
        }
124
        if (!$protocolVersion) {
125
            if (preg_match('/HTTP\/(?<version>\d+(\.\d+)?)/i', $_SERVER['SERVER_PROTOCOL'], $matches)) {
126
                $protocolVersion = $matches['version'];
127
            }
128
        }
129
        if ($protocolVersion) {
130
            $this->setProtocolVersion($protocolVersion);
131
        }
132
    }
133
134
    /**
135
     * The HTTP version used to respond to this request (typically 1.0 or 1.1)
136
     *
137
     * @param string $protocolVersion
138
     *
139
     * @return $this
140
     */
141
    public function setProtocolVersion($protocolVersion)
142
    {
143
        $this->protocolVersion = $protocolVersion;
144
        return $this;
145
    }
146
147
    /**
148
     * @param int $code
149
     * @param string $description Optional. See {@link setStatusDescription()}.
150
     *  No newlines are allowed in the description.
151
     *  If omitted, will default to the standard HTTP description
152
     *  for the given $code value (see {@link $status_codes}).
153
     *
154
     * @return $this
155
     */
156
    public function setStatusCode($code, $description = null)
157
    {
158
        if (isset(self::$status_codes[$code])) {
159
            $this->statusCode = $code;
160
        } else {
161
            throw new InvalidArgumentException("Unrecognised HTTP status code '$code'");
162
        }
163
164
        if ($description) {
165
            $this->statusDescription = $description;
166
        } else {
167
            $this->statusDescription = self::$status_codes[$code];
168
        }
169
        return $this;
170
    }
171
172
    /**
173
     * The text to be given alongside the status code ("reason phrase").
174
     * Caution: Will be overwritten by {@link setStatusCode()}.
175
     *
176
     * @param string $description
177
     *
178
     * @return $this
179
     */
180
    public function setStatusDescription($description)
181
    {
182
        $this->statusDescription = $description;
183
        return $this;
184
    }
185
186
    /**
187
     * @return string
188
     */
189
    public function getProtocolVersion()
190
    {
191
        return $this->protocolVersion;
192
    }
193
194
    /**
195
     * @return int
196
     */
197
    public function getStatusCode()
198
    {
199
        return $this->statusCode;
200
    }
201
202
    /**
203
     * @return string Description for a HTTP status code
204
     */
205
    public function getStatusDescription()
206
    {
207
        return str_replace(["\r", "\n"], '', $this->statusDescription);
208
    }
209
210
    /**
211
     * Returns true if this HTTP response is in error
212
     *
213
     * @return bool
214
     */
215
    public function isError()
216
    {
217
        $statusCode = $this->getStatusCode();
218
        return $statusCode && ($statusCode < 200 || $statusCode > 399);
219
    }
220
221
    /**
222
     * @param string $body
223
     *
224
     * @return $this
225
     */
226
    public function setBody($body)
227
    {
228
        $this->body = $body ? (string)$body : $body; // Don't type-cast false-ish values, eg null is null not ''
229
        return $this;
230
    }
231
232
    /**
233
     * @return string
234
     */
235
    public function getBody()
236
    {
237
        return $this->body;
238
    }
239
240
    /**
241
     * Add a HTTP header to the response, replacing any header of the same name.
242
     *
243
     * @param string $header Example: "content-type"
244
     * @param string $value Example: "text/xml"
245
     *
246
     * @return $this
247
     */
248
    public function addHeader($header, $value)
249
    {
250
        $header = strtolower($header);
251
        $this->headers[$header] = $value;
252
        return $this;
253
    }
254
255
    /**
256
     * Return the HTTP header of the given name.
257
     *
258
     * @param string $header
259
     *
260
     * @return string
261
     */
262
    public function getHeader($header)
263
    {
264
        $header = strtolower($header);
265
        if (isset($this->headers[$header])) {
266
            return $this->headers[$header];
267
        }
268
        return null;
269
    }
270
271
    /**
272
     * @return array
273
     */
274
    public function getHeaders()
275
    {
276
        return $this->headers;
277
    }
278
279
    /**
280
     * Remove an existing HTTP header by its name,
281
     * e.g. "Content-Type".
282
     *
283
     * @param string $header
284
     *
285
     * @return $this
286
     */
287
    public function removeHeader($header)
288
    {
289
        $header = strtolower($header);
290
        unset($this->headers[$header]);
291
        return $this;
292
    }
293
294
    /**
295
     * @param string $dest
296
     * @param int $code
297
     *
298
     * @return $this
299
     */
300
    public function redirect($dest, $code = 302)
301
    {
302
        if (!in_array($code, self::$redirect_codes)) {
303
            trigger_error("Invalid HTTP redirect code {$code}", E_USER_WARNING);
304
            $code = 302;
305
        }
306
        $this->setStatusCode($code);
307
        $this->addHeader('location', $dest);
308
        return $this;
309
    }
310
311
    /**
312
     * Send this HTTPResponse to the browser
313
     */
314
    public function output()
315
    {
316
        // Attach appropriate X-Include-JavaScript and X-Include-CSS headers
317
        if (Director::is_ajax()) {
318
            Requirements::include_in_response($this);
319
        }
320
321
        if ($this->isRedirect() && headers_sent()) {
322
            $this->htmlRedirect();
323
        } else {
324
            $this->outputHeaders();
325
            $this->outputBody();
326
        }
327
    }
328
329
    /**
330
     * Generate a browser redirect without setting headers
331
     */
332
    protected function htmlRedirect()
333
    {
334
        $headersSent = headers_sent($file, $line);
335
        $location = $this->getHeader('location');
336
        $url = Director::absoluteURL($location);
337
        $urlATT = Convert::raw2htmlatt($url);
338
        $urlJS = Convert::raw2js($url);
339
        $title = (Director::isDev() && $headersSent)
340
            ? "{$urlATT}... (output started on {$file}, line {$line})"
341
            : "{$urlATT}...";
342
        echo <<<EOT
343
<p>Redirecting to <a href="{$urlATT}" title="Click this link if your browser does not redirect you">{$title}</a></p>
344
<meta http-equiv="refresh" content="1; url={$urlATT}" />
345
<script type="application/javascript">setTimeout(function(){
346
	window.location.href = "{$urlJS}";
347
}, 50);</script>
348
EOT
349
        ;
350
    }
351
352
    /**
353
     * Output HTTP headers to the browser
354
     */
355
    protected function outputHeaders()
356
    {
357
        $headersSent = headers_sent($file, $line);
358
        if (!$headersSent) {
359
            $method = sprintf(
360
                "%s %d %s",
361
                $_SERVER['SERVER_PROTOCOL'],
362
                $this->getStatusCode(),
363
                $this->getStatusDescription()
364
            );
365
            header($method);
366
            foreach ($this->getHeaders() as $header => $value) {
367
                header("{$header}: {$value}", true, $this->getStatusCode());
368
            }
369
        } elseif ($this->getStatusCode() >= 300) {
370
            // It's critical that these status codes are sent; we need to report a failure if not.
371
            user_error(
372
                sprintf(
373
                    "Couldn't set response type to %d because of output on line %s of %s",
374
                    $this->getStatusCode(),
375
                    $line,
376
                    $file
377
                ),
378
                E_USER_WARNING
379
            );
380
        }
381
    }
382
383
    /**
384
     * Output body of this response to the browser
385
     */
386
    protected function outputBody()
387
    {
388
        // Only show error pages or generic "friendly" errors if the status code signifies
389
        // an error, and the response doesn't have any body yet that might contain
390
        // a more specific error description.
391
        $body = $this->getBody();
392
        if ($this->isError() && empty($body)) {
393
            /** @var HandlerInterface $handler */
394
            $handler = Injector::inst()->get(HandlerInterface::class);
395
            $formatter = $handler->getFormatter();
396
            echo $formatter->format([
397
                'code' => $this->statusCode,
398
            ]);
399
        } else {
400
            echo $this->body;
401
        }
402
    }
403
404
    /**
405
     * Returns true if this response is "finished", that is, no more script execution should be done.
406
     * Specifically, returns true if a redirect has already been requested
407
     *
408
     * @return bool
409
     */
410
    public function isFinished()
411
    {
412
        return $this->isRedirect() || $this->isError();
413
    }
414
415
    /**
416
     * Determine if this response is a redirect
417
     *
418
     * @return bool
419
     */
420
    public function isRedirect()
421
    {
422
        return in_array($this->getStatusCode(), self::$redirect_codes);
423
    }
424
425
    /**
426
     * The HTTP response represented as a raw string
427
     *
428
     * @return string
429
     */
430
    public function __toString()
431
    {
432
        $headers = [];
433
        foreach ($this->getHeaders() as $header => $values) {
434
            foreach ((array)$values as $value) {
435
                $headers[] = sprintf('%s: %s', $header, $value);
436
            }
437
        }
438
        return
439
            sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getStatusDescription()) . "\r\n" .
440
            implode("\r\n", $headers) . "\r\n" . "\r\n" .
441
            $this->getBody();
442
    }
443
}
444