Completed
Push — 4.0 ( b59aea...80f83b )
by Loz
52s queued 21s
created

HTTPResponse::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 3
dl 0
loc 5
rs 10
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 = array(
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 = array(
70
        301,
71
        302,
72
        303,
73
        304,
74
        305,
75
        307,
76
        308
77
    );
78
79
    /**
80
     * @var int
81
     */
82
    protected $statusCode = 200;
83
84
    /**
85
     * @var string
86
     */
87
    protected $statusDescription = "OK";
88
89
    /**
90
     * HTTP Headers like "content-type: text/xml"
91
     *
92
     * @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
93
     * @var array
94
     */
95
    protected $headers = array(
96
        "content-type" => "text/html; charset=utf-8",
97
    );
98
99
    /**
100
     * @var string
101
     */
102
    protected $body = null;
103
104
    /**
105
     * Create a new HTTP response
106
     *
107
     * @param string $body The body of the response
108
     * @param int $statusCode The numeric status code - 200, 404, etc
109
     * @param string $statusDescription The text to be given alongside the status code.
110
     *  See {@link setStatusCode()} for more information.
111
     */
112
    public function __construct($body = null, $statusCode = null, $statusDescription = null)
113
    {
114
        $this->setBody($body);
115
        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...
116
            $this->setStatusCode($statusCode, $statusDescription);
117
        }
118
    }
119
120
    /**
121
     * @param int $code
122
     * @param string $description Optional. See {@link setStatusDescription()}.
123
     *  No newlines are allowed in the description.
124
     *  If omitted, will default to the standard HTTP description
125
     *  for the given $code value (see {@link $status_codes}).
126
     * @return $this
127
     */
128
    public function setStatusCode($code, $description = null)
129
    {
130
        if (isset(self::$status_codes[$code])) {
131
            $this->statusCode = $code;
132
        } else {
133
            throw new InvalidArgumentException("Unrecognised HTTP status code '$code'");
134
        }
135
136
        if ($description) {
137
            $this->statusDescription = $description;
138
        } else {
139
            $this->statusDescription = self::$status_codes[$code];
140
        }
141
        return $this;
142
    }
143
144
    /**
145
     * The text to be given alongside the status code ("reason phrase").
146
     * Caution: Will be overwritten by {@link setStatusCode()}.
147
     *
148
     * @param string $description
149
     * @return $this
150
     */
151
    public function setStatusDescription($description)
152
    {
153
        $this->statusDescription = $description;
154
        return $this;
155
    }
156
157
    /**
158
     * @return int
159
     */
160
    public function getStatusCode()
161
    {
162
        return $this->statusCode;
163
    }
164
165
    /**
166
     * @return string Description for a HTTP status code
167
     */
168
    public function getStatusDescription()
169
    {
170
        return str_replace(array("\r","\n"), '', $this->statusDescription);
171
    }
172
173
    /**
174
     * Returns true if this HTTP response is in error
175
     *
176
     * @return bool
177
     */
178
    public function isError()
179
    {
180
        $statusCode = $this->getStatusCode();
181
        return $statusCode && ($statusCode < 200 || $statusCode > 399);
182
    }
183
184
    /**
185
     * @param string $body
186
     * @return $this
187
     */
188
    public function setBody($body)
189
    {
190
        $this->body = $body ? (string) $body : $body; // Don't type-cast false-ish values, eg null is null not ''
191
        return $this;
192
    }
193
194
    /**
195
     * @return string
196
     */
197
    public function getBody()
198
    {
199
        return $this->body;
200
    }
201
202
    /**
203
     * Add a HTTP header to the response, replacing any header of the same name.
204
     *
205
     * @param string $header Example: "content-type"
206
     * @param string $value Example: "text/xml"
207
     * @return $this
208
     */
209
    public function addHeader($header, $value)
210
    {
211
        $header = strtolower($header);
212
        $this->headers[$header] = $value;
213
        return $this;
214
    }
215
216
    /**
217
     * Return the HTTP header of the given name.
218
     *
219
     * @param string $header
220
     * @returns string
221
     */
222
    public function getHeader($header)
223
    {
224
        $header = strtolower($header);
225
        if (isset($this->headers[$header])) {
226
            return $this->headers[$header];
227
        }
228
        return null;
229
    }
230
231
    /**
232
     * @return array
233
     */
234
    public function getHeaders()
235
    {
236
        return $this->headers;
237
    }
238
239
    /**
240
     * Remove an existing HTTP header by its name,
241
     * e.g. "Content-Type".
242
     *
243
     * @param string $header
244
     * @return $this
245
     */
246
    public function removeHeader($header)
247
    {
248
        $header = strtolower($header);
249
        unset($this->headers[$header]);
250
        return $this;
251
    }
252
253
    /**
254
     * @param string $dest
255
     * @param int $code
256
     * @return $this
257
     */
258
    public function redirect($dest, $code = 302)
259
    {
260
        if (!in_array($code, self::$redirect_codes)) {
261
            trigger_error("Invalid HTTP redirect code {$code}", E_USER_WARNING);
262
            $code = 302;
263
        }
264
        $this->setStatusCode($code);
265
        $this->addHeader('location', $dest);
266
        return $this;
267
    }
268
269
    /**
270
     * Send this HTTPResponse to the browser
271
     */
272
    public function output()
273
    {
274
        // Attach appropriate X-Include-JavaScript and X-Include-CSS headers
275
        if (Director::is_ajax()) {
276
            Requirements::include_in_response($this);
277
        }
278
279
        if ($this->isRedirect() && headers_sent()) {
280
            $this->htmlRedirect();
281
        } else {
282
            $this->outputHeaders();
283
            $this->outputBody();
284
        }
285
    }
286
287
    /**
288
     * Generate a browser redirect without setting headers
289
     */
290
    protected function htmlRedirect()
291
    {
292
        $headersSent = headers_sent($file, $line);
293
        $location = $this->getHeader('location');
294
        $url = Director::absoluteURL($location);
295
        $urlATT = Convert::raw2htmlatt($url);
296
        $urlJS = Convert::raw2js($url);
297
        $title = (Director::isDev() && $headersSent)
298
            ? "{$urlATT}... (output started on {$file}, line {$line})"
299
            : "{$urlATT}...";
300
        echo <<<EOT
301
<p>Redirecting to <a href="{$urlATT}" title="Click this link if your browser does not redirect you">{$title}</a></p>
302
<meta http-equiv="refresh" content="1; url={$urlATT}" />
303
<script type="application/javascript">setTimeout(function(){
304
	window.location.href = "{$urlJS}";
305
}, 50);</script>
306
EOT
307
        ;
308
    }
309
310
    /**
311
     * Output HTTP headers to the browser
312
     */
313
    protected function outputHeaders()
314
    {
315
        $headersSent = headers_sent($file, $line);
316
        if (!$headersSent) {
317
            $method = sprintf(
318
                "%s %d %s",
319
                $_SERVER['SERVER_PROTOCOL'],
320
                $this->getStatusCode(),
321
                $this->getStatusDescription()
322
            );
323
            header($method);
324
            foreach ($this->getHeaders() as $header => $value) {
325
                    header("{$header}: {$value}", true, $this->getStatusCode());
326
            }
327
        } elseif ($this->getStatusCode() >= 300) {
328
            // It's critical that these status codes are sent; we need to report a failure if not.
329
            user_error(
330
                sprintf(
331
                    "Couldn't set response type to %d because of output on line %s of %s",
332
                    $this->getStatusCode(),
333
                    $line,
334
                    $file
335
                ),
336
                E_USER_WARNING
337
            );
338
        }
339
    }
340
341
    /**
342
     * Output body of this response to the browser
343
     */
344
    protected function outputBody()
345
    {
346
        // Only show error pages or generic "friendly" errors if the status code signifies
347
        // an error, and the response doesn't have any body yet that might contain
348
        // a more specific error description.
349
        $body = $this->getBody();
350
        if ($this->isError() && empty($body)) {
351
            /** @var HandlerInterface $handler */
352
            $handler = Injector::inst()->get(HandlerInterface::class);
353
            $formatter = $handler->getFormatter();
354
            echo $formatter->format(array(
355
                'code' => $this->statusCode
356
            ));
357
        } else {
358
            echo $this->body;
359
        }
360
    }
361
362
    /**
363
     * Returns true if this response is "finished", that is, no more script execution should be done.
364
     * Specifically, returns true if a redirect has already been requested
365
     *
366
     * @return bool
367
     */
368
    public function isFinished()
369
    {
370
        return $this->isRedirect() || $this->isError();
371
    }
372
373
    /**
374
     * Determine if this response is a redirect
375
     *
376
     * @return bool
377
     */
378
    public function isRedirect()
379
    {
380
        return in_array($this->getStatusCode(), self::$redirect_codes);
381
    }
382
}
383