Completed
Pull Request — master (#6909)
by Damian
26:02 queued 17:19
created

HTTPResponse::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 3
dl 0
loc 7
rs 9.4285
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
        400 => 'Bad Request',
39
        401 => 'Unauthorized',
40
        403 => 'Forbidden',
41
        404 => 'Not Found',
42
        405 => 'Method Not Allowed',
43
        406 => 'Not Acceptable',
44
        407 => 'Proxy Authentication Required',
45
        408 => 'Request Timeout',
46
        409 => 'Conflict',
47
        410 => 'Gone',
48
        411 => 'Length Required',
49
        412 => 'Precondition Failed',
50
        413 => 'Request Entity Too Large',
51
        414 => 'Request-URI Too Long',
52
        415 => 'Unsupported Media Type',
53
        416 => 'Request Range Not Satisfiable',
54
        417 => 'Expectation Failed',
55
        422 => 'Unprocessable Entity',
56
        429 => 'Too Many Requests',
57
        500 => 'Internal Server Error',
58
        501 => 'Not Implemented',
59
        502 => 'Bad Gateway',
60
        503 => 'Service Unavailable',
61
        504 => 'Gateway Timeout',
62
        505 => 'HTTP Version Not Supported',
63
    );
64
65
    /**
66
     * @var array
67
     */
68
    protected static $redirect_codes = array(
69
        301,
70
        302,
71
        303,
72
        304,
73
        305,
74
        307
75
    );
76
77
    /**
78
     * @var int
79
     */
80
    protected $statusCode = 200;
81
82
    /**
83
     * @var string
84
     */
85
    protected $statusDescription = "OK";
86
87
    /**
88
     * HTTP Headers like "Content-Type: text/xml"
89
     *
90
     * @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
91
     * @var array
92
     */
93
    protected $headers = array(
94
        "Content-Type" => "text/html; charset=utf-8",
95
    );
96
97
    /**
98
     * @var string
99
     */
100
    protected $body = null;
101
102
    /**
103
     * Create a new HTTP response
104
     *
105
     * @param string $body The body of the response
106
     * @param int $statusCode The numeric status code - 200, 404, etc
107
     * @param string $statusDescription The text to be given alongside the status code.
108
     *  See {@link setStatusCode()} for more information.
109
     */
110
    public function __construct($body = null, $statusCode = null, $statusDescription = null)
111
    {
112
        $this->setBody($body);
113
        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 zero. 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...
114
            $this->setStatusCode($statusCode, $statusDescription);
115
        }
116
    }
117
118
    /**
119
     * @param int $code
120
     * @param string $description Optional. See {@link setStatusDescription()}.
121
     *  No newlines are allowed in the description.
122
     *  If omitted, will default to the standard HTTP description
123
     *  for the given $code value (see {@link $status_codes}).
124
     * @return $this
125
     */
126
    public function setStatusCode($code, $description = null)
127
    {
128
        if (isset(self::$status_codes[$code])) {
129
            $this->statusCode = $code;
130
        } else {
131
            throw new InvalidArgumentException("Unrecognised HTTP status code '$code'");
132
        }
133
134
        if ($description) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $description of type string|null is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
135
            $this->statusDescription = $description;
136
        } else {
137
            $this->statusDescription = self::$status_codes[$code];
138
        }
139
        return $this;
140
    }
141
142
    /**
143
     * The text to be given alongside the status code ("reason phrase").
144
     * Caution: Will be overwritten by {@link setStatusCode()}.
145
     *
146
     * @param string $description
147
     * @return $this
148
     */
149
    public function setStatusDescription($description)
150
    {
151
        $this->statusDescription = $description;
152
        return $this;
153
    }
154
155
    /**
156
     * @return int
157
     */
158
    public function getStatusCode()
159
    {
160
        return $this->statusCode;
161
    }
162
163
    /**
164
     * @return string Description for a HTTP status code
165
     */
166
    public function getStatusDescription()
167
    {
168
        return str_replace(array("\r","\n"), '', $this->statusDescription);
169
    }
170
171
    /**
172
     * Returns true if this HTTP response is in error
173
     *
174
     * @return bool
175
     */
176
    public function isError()
177
    {
178
        $statusCode = $this->getStatusCode();
179
        return $statusCode && ($statusCode < 200 || $statusCode > 399);
180
    }
181
182
    /**
183
     * @param string $body
184
     * @return $this
185
     */
186
    public function setBody($body)
187
    {
188
        $this->body = $body ? (string) $body : $body; // Don't type-cast false-ish values, eg null is null not ''
189
        return $this;
190
    }
191
192
    /**
193
     * @return string
194
     */
195
    public function getBody()
196
    {
197
        return $this->body;
198
    }
199
200
    /**
201
     * Add a HTTP header to the response, replacing any header of the same name.
202
     *
203
     * @param string $header Example: "Content-Type"
204
     * @param string $value Example: "text/xml"
205
     * @return $this
206
     */
207
    public function addHeader($header, $value)
208
    {
209
        $this->headers[$header] = $value;
210
        return $this;
211
    }
212
213
    /**
214
     * Return the HTTP header of the given name.
215
     *
216
     * @param string $header
217
     * @returns string
218
     */
219
    public function getHeader($header)
220
    {
221
        if (isset($this->headers[$header])) {
222
            return $this->headers[$header];
223
        }
224
        return null;
225
    }
226
227
    /**
228
     * @return array
229
     */
230
    public function getHeaders()
231
    {
232
        return $this->headers;
233
    }
234
235
    /**
236
     * Remove an existing HTTP header by its name,
237
     * e.g. "Content-Type".
238
     *
239
     * @param string $header
240
     * @return $this
241
     */
242
    public function removeHeader($header)
243
    {
244
        unset($this->headers[$header]);
245
        return $this;
246
    }
247
248
    /**
249
     * @param string $dest
250
     * @param int $code
251
     * @return $this
252
     */
253
    public function redirect($dest, $code = 302)
254
    {
255
        if (!in_array($code, self::$redirect_codes)) {
256
            trigger_error("Invalid HTTP redirect code {$code}", E_USER_WARNING);
257
            $code = 302;
258
        }
259
        $this->setStatusCode($code);
260
        $this->addHeader('Location', $dest);
261
        return $this;
262
    }
263
264
    /**
265
     * Send this HTTPResponse to the browser
266
     */
267
    public function output()
268
    {
269
        // Attach appropriate X-Include-JavaScript and X-Include-CSS headers
270
        if (Director::is_ajax()) {
271
            Requirements::include_in_response($this);
272
        }
273
274
        if ($this->isRedirect() && headers_sent()) {
275
            $this->htmlRedirect();
276
        } else {
277
            $this->outputHeaders();
278
            $this->outputBody();
279
        }
280
    }
281
282
    /**
283
     * Generate a browser redirect without setting headers
284
     */
285
    protected function htmlRedirect()
286
    {
287
        $headersSent = headers_sent($file, $line);
288
        $location = $this->getHeader('Location');
289
        $url = Director::absoluteURL($location);
290
        $urlATT = Convert::raw2htmlatt($url);
0 ignored issues
show
Security Bug introduced by
It seems like $url defined by \SilverStripe\Control\Di...:absoluteURL($location) on line 289 can also be of type false; however, SilverStripe\Core\Convert::raw2htmlatt() does only seem to accept string|array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
291
        $urlJS = Convert::raw2js($url);
0 ignored issues
show
Security Bug introduced by
It seems like $url defined by \SilverStripe\Control\Di...:absoluteURL($location) on line 289 can also be of type false; however, SilverStripe\Core\Convert::raw2js() does only seem to accept array|string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
292
        $title = (Director::isDev() && $headersSent)
293
            ? "{$urlATT}... (output started on {$file}, line {$line})"
294
            : "{$urlATT}...";
295
        echo <<<EOT
296
<p>Redirecting to <a href="{$urlATT}" title="Click this link if your browser does not redirect you">{$title}</a></p>
297
<meta http-equiv="refresh" content="1; url={$urlATT}" />
298
<script type="application/javascript">setTimeout(function(){
299
	window.location.href = "{$urlJS}";
300
}, 50);</script>
301
EOT
302
        ;
303
    }
304
305
    /**
306
     * Output HTTP headers to the browser
307
     */
308
    protected function outputHeaders()
0 ignored issues
show
Coding Style introduced by
outputHeaders uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
309
    {
310
        $headersSent = headers_sent($file, $line);
311
        if (!$headersSent) {
312
            $method = sprintf(
313
                "%s %d %s",
314
                $_SERVER['SERVER_PROTOCOL'],
315
                $this->getStatusCode(),
316
                $this->getStatusDescription()
317
            );
318
            header($method);
319
            foreach ($this->getHeaders() as $header => $value) {
320
                    header("{$header}: {$value}", true, $this->getStatusCode());
321
            }
322
        } elseif ($this->getStatusCode() >= 300) {
323
            // It's critical that these status codes are sent; we need to report a failure if not.
324
            user_error(
325
                sprintf(
326
                    "Couldn't set response type to %d because of output on line %s of %s",
327
                    $this->getStatusCode(),
328
                    $line,
329
                    $file
330
                ),
331
                E_USER_WARNING
332
            );
333
        }
334
    }
335
336
    /**
337
     * Output body of this response to the browser
338
     */
339
    protected function outputBody()
340
    {
341
        // Only show error pages or generic "friendly" errors if the status code signifies
342
        // an error, and the response doesn't have any body yet that might contain
343
        // a more specific error description.
344
        $body = $this->getBody();
345
        if ($this->isError() && empty($body)) {
346
            /** @var HandlerInterface $handler */
347
            $handler = Injector::inst()->get(HandlerInterface::class);
348
            $formatter = $handler->getFormatter();
349
            echo $formatter->format(array(
350
                'code' => $this->statusCode
351
            ));
352
        } else {
353
            echo $this->body;
354
        }
355
    }
356
357
    /**
358
     * Returns true if this response is "finished", that is, no more script execution should be done.
359
     * Specifically, returns true if a redirect has already been requested
360
     *
361
     * @return bool
362
     */
363
    public function isFinished()
364
    {
365
        return $this->isRedirect() || $this->isError();
366
    }
367
368
    /**
369
     * Determine if this response is a redirect
370
     *
371
     * @return bool
372
     */
373
    public function isRedirect()
374
    {
375
        return in_array($this->getStatusCode(), self::$redirect_codes);
376
    }
377
}
378