Completed
Push — namespace-model ( 32fe71...476d0e )
by Sam
16:05
created

HTTPResponse::getBody()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
namespace SilverStripe\Control;
4
use Requirements;
5
use Convert;
6
use Injector;
7
8
use Exception;
9
use SilverStripe\Control\HTTPResponse;
10
11
12
13
/**
14
 * Represents a response returned by a controller.
15
 *
16
 * @package framework
17
 * @subpackage control
18
 */
19
class HTTPResponse {
20
21
	/**
22
	 * @var array
23
	 */
24
	protected static $status_codes = array(
25
		100 => 'Continue',
26
		101 => 'Switching Protocols',
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
		301 => 'Moved Permanently',
35
		302 => 'Found',
36
		303 => 'See Other',
37
		304 => 'Not Modified',
38
		305 => 'Use Proxy',
39
		307 => 'Temporary Redirect',
40
		400 => 'Bad Request',
41
		401 => 'Unauthorized',
42
		403 => 'Forbidden',
43
		404 => 'Not Found',
44
		405 => 'Method Not Allowed',
45
		406 => 'Not Acceptable',
46
		407 => 'Proxy Authentication Required',
47
		408 => 'Request Timeout',
48
		409 => 'Conflict',
49
		410 => 'Gone',
50
		411 => 'Length Required',
51
		412 => 'Precondition Failed',
52
		413 => 'Request Entity Too Large',
53
		414 => 'Request-URI Too Long',
54
		415 => 'Unsupported Media Type',
55
		416 => 'Request Range Not Satisfiable',
56
		417 => 'Expectation Failed',
57
		422 => 'Unprocessable Entity',
58
		429 => 'Too Many Requests',
59
		500 => 'Internal Server Error',
60
		501 => 'Not Implemented',
61
		502 => 'Bad Gateway',
62
		503 => 'Service Unavailable',
63
		504 => 'Gateway Timeout',
64
		505 => 'HTTP Version Not Supported',
65
	);
66
67
	/**
68
	 * @var array
69
	 */
70
	protected static $redirect_codes = array(
71
		301,
72
		302,
73
		303,
74
		304,
75
		305,
76
		307
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 $body The body of the response
108
	 * @param $statusCode The numeric status code - 200, 404, etc
109
	 * @param $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
		$this->setBody($body);
114
		if($statusCode) $this->setStatusCode($statusCode, $statusDescription);
115
	}
116
117
	/**
118
	 * @param string $code
119
	 * @param string $description Optional. See {@link setStatusDescription()}.
120
	 *  No newlines are allowed in the description.
121
	 *  If omitted, will default to the standard HTTP description
122
	 *  for the given $code value (see {@link $status_codes}).
123
	 * @return SS_HTTPRequest $this
124
	 */
125
	public function setStatusCode($code, $description = null) {
126
		if(isset(self::$status_codes[$code])) $this->statusCode = $code;
0 ignored issues
show
Documentation Bug introduced by
The property $statusCode was declared of type integer, but $code is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
127
		else user_error("Unrecognised HTTP status code '$code'", E_USER_WARNING);
128
129
		if($description) $this->statusDescription = $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...
130
		else $this->statusDescription = self::$status_codes[$code];
131
		return $this;
132
	}
133
134
	/**
135
	 * The text to be given alongside the status code ("reason phrase").
136
	 * Caution: Will be overwritten by {@link setStatusCode()}.
137
	 *
138
	 * @param string $description
139
	 * @return SS_HTTPRequest $this
140
	 */
141
	public function setStatusDescription($description) {
142
		$this->statusDescription = $description;
143
		return $this;
144
	}
145
146
	/**
147
	 * @return int
148
	 */
149
	public function getStatusCode() {
150
		return $this->statusCode;
151
	}
152
153
	/**
154
	 * @return string Description for a HTTP status code
155
	 */
156
	public function getStatusDescription() {
157
		return str_replace(array("\r","\n"), '', $this->statusDescription);
158
	}
159
160
	/**
161
	 * Returns true if this HTTP response is in error
162
	 *
163
	 * @return bool
164
	 */
165
	public function isError() {
166
		return $this->statusCode && ($this->statusCode < 200 || $this->statusCode > 399);
167
	}
168
169
	/**
170
	 * @param string $body
171
	 * @return SS_HTTPRequest $this
172
	 */
173
	public function setBody($body) {
174
		$this->body = $body ? (string) $body : $body; // Don't type-cast false-ish values, eg null is null not ''
175
		return $this;
176
	}
177
178
	/**
179
	 * @return null|string
180
	 */
181
	public function getBody() {
182
		return $this->body;
183
	}
184
185
	/**
186
	 * Add a HTTP header to the response, replacing any header of the same name.
187
	 *
188
	 * @param string $header Example: "Content-Type"
189
	 * @param string $value Example: "text/xml"
190
	 * @return SS_HTTPRequest $this
191
	 */
192
	public function addHeader($header, $value) {
193
		$this->headers[$header] = $value;
194
		return $this;
195
	}
196
197
	/**
198
	 * Return the HTTP header of the given name.
199
	 *
200
	 * @param string $header
201
	 * @returns null|string
202
	 */
203
	public function getHeader($header) {
204
		if(isset($this->headers[$header]))
205
			return $this->headers[$header];
206
		}
207
208
	/**
209
	 * @return array
210
	 */
211
	public function getHeaders() {
212
		return $this->headers;
213
	}
214
215
	/**
216
	 * Remove an existing HTTP header by its name,
217
	 * e.g. "Content-Type".
218
	 *
219
	 * @param string $header
220
	 * @return SS_HTTPRequest $this
221
	 */
222
	public function removeHeader($header) {
223
		if(isset($this->headers[$header])) unset($this->headers[$header]);
224
		return $this;
225
	}
226
227
	/**
228
	 * @param string $dest
229
	 * @param int $code
230
	 * @return SS_HTTPRequest $this
231
	 */
232
	public function redirect($dest, $code=302) {
233
		if(!in_array($code, self::$redirect_codes)) $code = 302;
234
		$this->setStatusCode($code);
235
		$this->headers['Location'] = $dest;
236
		return $this;
237
	}
238
239
	/**
240
	 * Send this HTTPReponse to the browser
241
	 */
242
	public function output() {
243
		// Attach appropriate X-Include-JavaScript and X-Include-CSS headers
244
		if(Director::is_ajax()) {
245
			Requirements::include_in_response($this);
246
		}
247
248
		if(in_array($this->statusCode, self::$redirect_codes) && headers_sent($file, $line)) {
249
			$url = Director::absoluteURL($this->headers['Location'], true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
250
			$urlATT = Convert::raw2htmlatt($url);
0 ignored issues
show
Security Bug introduced by
It seems like $url defined by \SilverStripe\Control\Di...ders['Location'], true) on line 249 can also be of type false; however, 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...
251
			$urlJS = Convert::raw2js($url);
0 ignored issues
show
Security Bug introduced by
It seems like $url defined by \SilverStripe\Control\Di...ders['Location'], true) on line 249 can also be of type false; however, 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...
252
			$title = Director::isDev()
253
				? "{$urlATT}... (output started on {$file}, line {$line})"
254
				: "{$urlATT}...";
255
			echo <<<EOT
256
<p>Redirecting to <a href="{$urlATT}" title="Click this link if your browser does not redirect you">{$title}</a></p>
257
<meta http-equiv="refresh" content="1; url={$urlATT}" />
258
<script type="application/javascript">setTimeout(function(){
259
	window.location.href = "{$urlJS}";
260
}, 50);</script>
261
EOT
262
			;
263
		} else {
264
			$line = $file = null;
265
			if(!headers_sent($file, $line)) {
266
				header($_SERVER['SERVER_PROTOCOL'] . " $this->statusCode " . $this->getStatusDescription());
267
				foreach($this->headers as $header => $value) {
268
					header("$header: $value", true, $this->statusCode);
269
				}
270
			} else {
271
				// It's critical that these status codes are sent; we need to report a failure if not.
272
				if($this->statusCode >= 300) {
273
					user_error(
274
						"Couldn't set response type to $this->statusCode because " .
275
						"of output on line $line of $file",
276
						E_USER_WARNING
277
					);
278
				}
279
			}
280
281
			// Only show error pages or generic "friendly" errors if the status code signifies
282
			// an error, and the response doesn't have any body yet that might contain
283
			// a more specific error description.
284
			if(Director::isLive() && $this->isError() && !$this->body) {
285
				$formatter = Injector::inst()->get('FriendlyErrorFormatter');
286
				echo $formatter->format(array(
287
					'code' => $this->statusCode
288
				));
289
290
			} else {
291
				echo $this->body;
292
			}
293
294
		}
295
	}
296
297
	/**
298
	 * Returns true if this response is "finished", that is, no more script execution should be done.
299
	 * Specifically, returns true if a redirect has already been requested
300
	 *
301
	 * @return bool
302
	 */
303
	public function isFinished() {
304
		return in_array($this->statusCode, array(301, 302, 303, 304, 305, 307, 401, 403));
305
	}
306
307
}
308
309
/**
310
 * A {@link SS_HTTPResponse} encapsulated in an exception, which can interrupt the processing flow and be caught by the
311
 * {@link RequestHandler} and returned to the user.
312
 *
313
 * Example Usage:
314
 * <code>
315
 * throw new SS_HTTPResponse_Exception('This request was invalid.', 400);
316
 * throw new SS_HTTPResponse_Exception(new SS_HTTPResponse('There was an internal server error.', 500));
317
 * </code>
318
 *
319
 * @package framework
320
 * @subpackage control
321
 */
322
class HTTPResponse_Exception extends Exception {
323
324
	protected $response;
325
326
	/**
327
	 * @param string|SS_HTTPResponse body Either the plaintext content of the error message, or an SS_HTTPResponse
328
	 *                                     object representing it.  In either case, the $statusCode and
329
	 *                                     $statusDescription will be the HTTP status of the resulting response.
330
	 * @see SS_HTTPResponse::__construct();
331
	 */
332
	public function __construct($body = null, $statusCode = null, $statusDescription = null) {
333
		if($body instanceof HTTPResponse) {
334
			// statusCode and statusDescription should override whatever is passed in the body
335
			if($statusCode) $body->setStatusCode($statusCode);
336
			if($statusDescription) $body->setStatusDescription($statusDescription);
337
338
			$this->setResponse($body);
339
		} else {
340
			$response = new HTTPResponse($body, $statusCode, $statusDescription);
341
342
			// Error responses should always be considered plaintext, for security reasons
343
			$response->addHeader('Content-Type', 'text/plain');
344
345
			$this->setResponse($response);
346
		}
347
348
		parent::__construct($this->getResponse()->getBody(), $this->getResponse()->getStatusCode());
349
	}
350
351
	/**
352
	 * @return SS_HTTPResponse
353
	 */
354
	public function getResponse() {
355
		return $this->response;
356
	}
357
358
	/**
359
	 * @param SS_HTTPResponse $response
360
	 */
361
	public function setResponse(HTTPResponse $response) {
362
		$this->response = $response;
363
	}
364
365
}
366