HTTPResponse   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 450
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 183
dl 0
loc 450
rs 8.72
c 0
b 0
f 0
wmc 46

22 Methods

Rating   Name   Duplication   Size   Complexity  
A output() 0 12 4
A getBody() 0 3 1
A setStatusCode() 0 14 3
A addHeader() 0 5 1
A getHeader() 0 7 2
A outputHeaders() 0 24 4
A htmlRedirect() 0 15 3
A getProtocolVersion() 0 3 1
A setBody() 0 4 2
A setProtocolVersion() 0 4 1
A getStatusDescription() 0 3 1
A outputBody() 0 15 3
A isRedirect() 0 3 1
A isFinished() 0 3 2
A setStatusDescription() 0 4 1
A removeHeader() 0 5 1
A __construct() 0 13 5
A getStatusCode() 0 3 1
A redirect() 0 9 2
A getHeaders() 0 3 1
A isError() 0 4 3
A __toString() 0 16 3

How to fix   Complexity   

Complex Class

Complex classes like HTTPResponse often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HTTPResponse, and based on these observations, apply Extract Interface, too.

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
        102 => 'Processing',
26
        103 => 'Early Hints',
27
        200 => 'OK',
28
        201 => 'Created',
29
        202 => 'Accepted',
30
        203 => 'Non-Authoritative Information',
31
        204 => 'No Content',
32
        205 => 'Reset Content',
33
        206 => 'Partial Content',
34
        207 => 'Multi-Status',
35
        208 => 'Already Reported',
36
        226 => 'IM Used',
37
        301 => 'Moved Permanently',
38
        302 => 'Found',
39
        303 => 'See Other',
40
        304 => 'Not Modified',
41
        305 => 'Use Proxy',
42
        307 => 'Temporary Redirect',
43
        308 => 'Permanent Redirect',
44
        400 => 'Bad Request',
45
        401 => 'Unauthorized',
46
        402 => 'Payment Required',
47
        403 => 'Forbidden',
48
        404 => 'Not Found',
49
        405 => 'Method Not Allowed',
50
        406 => 'Not Acceptable',
51
        407 => 'Proxy Authentication Required',
52
        408 => 'Request Timeout',
53
        409 => 'Conflict',
54
        410 => 'Gone',
55
        411 => 'Length Required',
56
        412 => 'Precondition Failed',
57
        413 => 'Request Entity Too Large',
58
        414 => 'Request-URI Too Long',
59
        415 => 'Unsupported Media Type',
60
        416 => 'Request Range Not Satisfiable',
61
        417 => 'Expectation Failed',
62
        418 => 'I\'m a Teapot',
63
        421 => 'Misdirected Request',
64
        422 => 'Unprocessable Entity',
65
        423 => 'Locked',
66
        424 => 'Failed Dependency',
67
        426 => 'Upgrade Required',
68
        428 => 'Precondition Required',
69
        429 => 'Too Many Requests',
70
        431 => 'Request Header Fields Too Large',
71
        451 => 'Unavailable For Legal Reasons',
72
        500 => 'Internal Server Error',
73
        501 => 'Not Implemented',
74
        502 => 'Bad Gateway',
75
        503 => 'Service Unavailable',
76
        504 => 'Gateway Timeout',
77
        505 => 'HTTP Version Not Supported',
78
        506 => 'Variant Also Negotiates',
79
        507 => 'Unsufficient Storage',
80
        508 => 'Loop Detected',
81
        510 => 'Not Extended',
82
        511 => 'Network Authentication Required',
83
    ];
84
85
    /**
86
     * @var array
87
     */
88
    protected static $redirect_codes = [
89
        301,
90
        302,
91
        303,
92
        304,
93
        305,
94
        307,
95
        308,
96
    ];
97
98
    /**
99
     * @var string
100
     */
101
    protected $protocolVersion = '1.0';
102
103
    /**
104
     * @var int
105
     */
106
    protected $statusCode = 200;
107
108
    /**
109
     * @var string
110
     */
111
    protected $statusDescription = "OK";
112
113
    /**
114
     * HTTP Headers like "content-type: text/xml"
115
     *
116
     * @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
117
     * @var array
118
     */
119
    protected $headers = [
120
        "content-type" => "text/html; charset=utf-8",
121
    ];
122
123
    /**
124
     * @var string
125
     */
126
    protected $body = null;
127
128
    /**
129
     * Create a new HTTP response
130
     *
131
     * @param string $body The body of the response
132
     * @param int $statusCode The numeric status code - 200, 404, etc
133
     * @param string $statusDescription The text to be given alongside the status code.
134
     *  See {@link setStatusCode()} for more information.
135
     * @param string $protocolVersion
136
     */
137
    public function __construct($body = null, $statusCode = null, $statusDescription = null, $protocolVersion = null)
138
    {
139
        $this->setBody($body);
140
        if ($statusCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $statusCode of type integer|null 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...
141
            $this->setStatusCode($statusCode, $statusDescription);
142
        }
143
        if (!$protocolVersion) {
144
            if (preg_match('/HTTP\/(?<version>\d+(\.\d+)?)/i', $_SERVER['SERVER_PROTOCOL'], $matches)) {
145
                $protocolVersion = $matches['version'];
146
            }
147
        }
148
        if ($protocolVersion) {
149
            $this->setProtocolVersion($protocolVersion);
150
        }
151
    }
152
153
    /**
154
     * The HTTP version used to respond to this request (typically 1.0 or 1.1)
155
     *
156
     * @param string $protocolVersion
157
     *
158
     * @return $this
159
     */
160
    public function setProtocolVersion($protocolVersion)
161
    {
162
        $this->protocolVersion = $protocolVersion;
163
        return $this;
164
    }
165
166
    /**
167
     * @param int $code
168
     * @param string $description Optional. See {@link setStatusDescription()}.
169
     *  No newlines are allowed in the description.
170
     *  If omitted, will default to the standard HTTP description
171
     *  for the given $code value (see {@link $status_codes}).
172
     *
173
     * @return $this
174
     */
175
    public function setStatusCode($code, $description = null)
176
    {
177
        if (isset(self::$status_codes[$code])) {
178
            $this->statusCode = $code;
179
        } else {
180
            throw new InvalidArgumentException("Unrecognised HTTP status code '$code'");
181
        }
182
183
        if ($description) {
184
            $this->statusDescription = $description;
185
        } else {
186
            $this->statusDescription = self::$status_codes[$code];
187
        }
188
        return $this;
189
    }
190
191
    /**
192
     * The text to be given alongside the status code ("reason phrase").
193
     * Caution: Will be overwritten by {@link setStatusCode()}.
194
     *
195
     * @param string $description
196
     *
197
     * @return $this
198
     */
199
    public function setStatusDescription($description)
200
    {
201
        $this->statusDescription = $description;
202
        return $this;
203
    }
204
205
    /**
206
     * @return string
207
     */
208
    public function getProtocolVersion()
209
    {
210
        return $this->protocolVersion;
211
    }
212
213
    /**
214
     * @return int
215
     */
216
    public function getStatusCode()
217
    {
218
        return $this->statusCode;
219
    }
220
221
    /**
222
     * @return string Description for a HTTP status code
223
     */
224
    public function getStatusDescription()
225
    {
226
        return str_replace(["\r", "\n"], '', $this->statusDescription);
227
    }
228
229
    /**
230
     * Returns true if this HTTP response is in error
231
     *
232
     * @return bool
233
     */
234
    public function isError()
235
    {
236
        $statusCode = $this->getStatusCode();
237
        return $statusCode && ($statusCode < 200 || $statusCode > 399);
238
    }
239
240
    /**
241
     * @param string $body
242
     *
243
     * @return $this
244
     */
245
    public function setBody($body)
246
    {
247
        $this->body = $body ? (string)$body : $body; // Don't type-cast false-ish values, eg null is null not ''
248
        return $this;
249
    }
250
251
    /**
252
     * @return string
253
     */
254
    public function getBody()
255
    {
256
        return $this->body;
257
    }
258
259
    /**
260
     * Add a HTTP header to the response, replacing any header of the same name.
261
     *
262
     * @param string $header Example: "content-type"
263
     * @param string $value Example: "text/xml"
264
     *
265
     * @return $this
266
     */
267
    public function addHeader($header, $value)
268
    {
269
        $header = strtolower($header);
270
        $this->headers[$header] = $value;
271
        return $this;
272
    }
273
274
    /**
275
     * Return the HTTP header of the given name.
276
     *
277
     * @param string $header
278
     *
279
     * @return string
280
     */
281
    public function getHeader($header)
282
    {
283
        $header = strtolower($header);
284
        if (isset($this->headers[$header])) {
285
            return $this->headers[$header];
286
        }
287
        return null;
288
    }
289
290
    /**
291
     * @return array
292
     */
293
    public function getHeaders()
294
    {
295
        return $this->headers;
296
    }
297
298
    /**
299
     * Remove an existing HTTP header by its name,
300
     * e.g. "Content-Type".
301
     *
302
     * @param string $header
303
     *
304
     * @return $this
305
     */
306
    public function removeHeader($header)
307
    {
308
        $header = strtolower($header);
309
        unset($this->headers[$header]);
310
        return $this;
311
    }
312
313
    /**
314
     * @param string $dest
315
     * @param int $code
316
     *
317
     * @return $this
318
     */
319
    public function redirect($dest, $code = 302)
320
    {
321
        if (!in_array($code, self::$redirect_codes)) {
322
            trigger_error("Invalid HTTP redirect code {$code}", E_USER_WARNING);
323
            $code = 302;
324
        }
325
        $this->setStatusCode($code);
326
        $this->addHeader('location', $dest);
327
        return $this;
328
    }
329
330
    /**
331
     * Send this HTTPResponse to the browser
332
     */
333
    public function output()
334
    {
335
        // Attach appropriate X-Include-JavaScript and X-Include-CSS headers
336
        if (Director::is_ajax()) {
337
            Requirements::include_in_response($this);
338
        }
339
340
        if ($this->isRedirect() && headers_sent()) {
341
            $this->htmlRedirect();
342
        } else {
343
            $this->outputHeaders();
344
            $this->outputBody();
345
        }
346
    }
347
348
    /**
349
     * Generate a browser redirect without setting headers
350
     */
351
    protected function htmlRedirect()
352
    {
353
        $headersSent = headers_sent($file, $line);
354
        $location = $this->getHeader('location');
355
        $url = Director::absoluteURL($location);
356
        $urlATT = Convert::raw2htmlatt($url);
357
        $urlJS = Convert::raw2js($url);
358
        $title = (Director::isDev() && $headersSent)
359
            ? "{$urlATT}... (output started on {$file}, line {$line})"
360
            : "{$urlATT}...";
361
        echo <<<EOT
362
<p>Redirecting to <a href="{$urlATT}" title="Click this link if your browser does not redirect you">{$title}</a></p>
363
<meta http-equiv="refresh" content="1; url={$urlATT}" />
364
<script type="application/javascript">setTimeout(function(){
365
	window.location.href = "{$urlJS}";
366
}, 50);</script>
367
EOT
368
        ;
369
    }
370
371
    /**
372
     * Output HTTP headers to the browser
373
     */
374
    protected function outputHeaders()
375
    {
376
        $headersSent = headers_sent($file, $line);
377
        if (!$headersSent) {
378
            $method = sprintf(
379
                "%s %d %s",
380
                $_SERVER['SERVER_PROTOCOL'],
381
                $this->getStatusCode(),
382
                $this->getStatusDescription()
383
            );
384
            header($method);
385
            foreach ($this->getHeaders() as $header => $value) {
386
                header("{$header}: {$value}", true, $this->getStatusCode());
387
            }
388
        } elseif ($this->getStatusCode() >= 300) {
389
            // It's critical that these status codes are sent; we need to report a failure if not.
390
            user_error(
391
                sprintf(
392
                    "Couldn't set response type to %d because of output on line %s of %s",
393
                    $this->getStatusCode(),
394
                    $line,
395
                    $file
396
                ),
397
                E_USER_WARNING
398
            );
399
        }
400
    }
401
402
    /**
403
     * Output body of this response to the browser
404
     */
405
    protected function outputBody()
406
    {
407
        // Only show error pages or generic "friendly" errors if the status code signifies
408
        // an error, and the response doesn't have any body yet that might contain
409
        // a more specific error description.
410
        $body = $this->getBody();
411
        if ($this->isError() && empty($body)) {
412
            /** @var HandlerInterface $handler */
413
            $handler = Injector::inst()->get(HandlerInterface::class);
414
            $formatter = $handler->getFormatter();
415
            echo $formatter->format([
416
                'code' => $this->statusCode,
417
            ]);
418
        } else {
419
            echo $this->body;
420
        }
421
    }
422
423
    /**
424
     * Returns true if this response is "finished", that is, no more script execution should be done.
425
     * Specifically, returns true if a redirect has already been requested
426
     *
427
     * @return bool
428
     */
429
    public function isFinished()
430
    {
431
        return $this->isRedirect() || $this->isError();
432
    }
433
434
    /**
435
     * Determine if this response is a redirect
436
     *
437
     * @return bool
438
     */
439
    public function isRedirect()
440
    {
441
        return in_array($this->getStatusCode(), self::$redirect_codes);
442
    }
443
444
    /**
445
     * The HTTP response represented as a raw string
446
     *
447
     * @return string
448
     */
449
    public function __toString()
450
    {
451
        $headers = [];
452
        foreach ($this->getHeaders() as $header => $values) {
453
            foreach ((array)$values as $value) {
454
                $headers[] = sprintf('%s: %s', $header, $value);
455
            }
456
        }
457
        return sprintf(
458
            'HTTP/%s %s %s',
459
            $this->getProtocolVersion(),
460
            $this->getStatusCode(),
461
            $this->getStatusDescription()
462
        ) . "\r\n"
463
            . implode("\r\n", $headers) . "\r\n" . "\r\n"
464
            . $this->getBody();
465
    }
466
}
467