Completed
Push — master ( f5d71d...bfd9cb )
by Sam
12:52
created

HTTPResponse::output()   C

Complexity

Conditions 13
Paths 16

Size

Total Lines 58
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 35
c 1
b 0
f 0
nc 16
nop 0
dl 0
loc 58
rs 6.5104

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Control;
4
5
use InvalidArgumentException;
6
use SilverStripe\Core\Convert;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\View\Requirements;
9
10
/**
11
 * Represents a response returned by a controller.
12
 */
13
class HTTPResponse {
14
15
	/**
16
	 * @var array
17
	 */
18
	protected static $status_codes = array(
19
		100 => 'Continue',
20
		101 => 'Switching Protocols',
21
		200 => 'OK',
22
		201 => 'Created',
23
		202 => 'Accepted',
24
		203 => 'Non-Authoritative Information',
25
		204 => 'No Content',
26
		205 => 'Reset Content',
27
		206 => 'Partial Content',
28
		301 => 'Moved Permanently',
29
		302 => 'Found',
30
		303 => 'See Other',
31
		304 => 'Not Modified',
32
		305 => 'Use Proxy',
33
		307 => 'Temporary Redirect',
34
		400 => 'Bad Request',
35
		401 => 'Unauthorized',
36
		403 => 'Forbidden',
37
		404 => 'Not Found',
38
		405 => 'Method Not Allowed',
39
		406 => 'Not Acceptable',
40
		407 => 'Proxy Authentication Required',
41
		408 => 'Request Timeout',
42
		409 => 'Conflict',
43
		410 => 'Gone',
44
		411 => 'Length Required',
45
		412 => 'Precondition Failed',
46
		413 => 'Request Entity Too Large',
47
		414 => 'Request-URI Too Long',
48
		415 => 'Unsupported Media Type',
49
		416 => 'Request Range Not Satisfiable',
50
		417 => 'Expectation Failed',
51
		422 => 'Unprocessable Entity',
52
		429 => 'Too Many Requests',
53
		500 => 'Internal Server Error',
54
		501 => 'Not Implemented',
55
		502 => 'Bad Gateway',
56
		503 => 'Service Unavailable',
57
		504 => 'Gateway Timeout',
58
		505 => 'HTTP Version Not Supported',
59
	);
60
61
	/**
62
	 * @var array
63
	 */
64
	protected static $redirect_codes = array(
65
		301,
66
		302,
67
		303,
68
		304,
69
		305,
70
		307
71
	);
72
73
	/**
74
	 * @var int
75
	 */
76
	protected $statusCode = 200;
77
78
	/**
79
	 * @var string
80
	 */
81
	protected $statusDescription = "OK";
82
83
	/**
84
	 * HTTP Headers like "Content-Type: text/xml"
85
	 *
86
	 * @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
87
	 * @var array
88
	 */
89
	protected $headers = array(
90
		"Content-Type" => "text/html; charset=utf-8",
91
	);
92
93
	/**
94
	 * @var string
95
	 */
96
	protected $body = null;
97
98
	/**
99
	 * Create a new HTTP response
100
	 *
101
	 * @param string $body The body of the response
102
	 * @param int $statusCode The numeric status code - 200, 404, etc
103
	 * @param string $statusDescription The text to be given alongside the status code.
104
	 *  See {@link setStatusCode()} for more information.
105
	 */
106
	public function __construct($body = null, $statusCode = null, $statusDescription = null) {
107
		$this->setBody($body);
108
		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...
109
			$this->setStatusCode($statusCode, $statusDescription);
110
		}
111
	}
112
113
	/**
114
	 * @param string $code
115
	 * @param string $description Optional. See {@link setStatusDescription()}.
116
	 *  No newlines are allowed in the description.
117
	 *  If omitted, will default to the standard HTTP description
118
	 *  for the given $code value (see {@link $status_codes}).
119
	 * @return $this
120
	 */
121
	public function setStatusCode($code, $description = null) {
122
		if(isset(self::$status_codes[$code])) {
123
			$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...
124
		} else {
125
			throw new InvalidArgumentException("Unrecognised HTTP status code '$code'");
126
		}
127
128
		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...
129
			$this->statusDescription = $description;
130
		} else {
131
			$this->statusDescription = self::$status_codes[$code];
132
		}
133
		return $this;
134
	}
135
136
	/**
137
	 * The text to be given alongside the status code ("reason phrase").
138
	 * Caution: Will be overwritten by {@link setStatusCode()}.
139
	 *
140
	 * @param string $description
141
	 * @return $this
142
	 */
143
	public function setStatusDescription($description) {
144
		$this->statusDescription = $description;
145
		return $this;
146
	}
147
148
	/**
149
	 * @return int
150
	 */
151
	public function getStatusCode() {
152
		return $this->statusCode;
153
	}
154
155
	/**
156
	 * @return string Description for a HTTP status code
157
	 */
158
	public function getStatusDescription() {
159
		return str_replace(array("\r","\n"), '', $this->statusDescription);
160
	}
161
162
	/**
163
	 * Returns true if this HTTP response is in error
164
	 *
165
	 * @return bool
166
	 */
167
	public function isError() {
168
		return $this->statusCode && ($this->statusCode < 200 || $this->statusCode > 399);
169
	}
170
171
	/**
172
	 * @param string $body
173
	 * @return $this
174
	 */
175
	public function setBody($body) {
176
		$this->body = $body ? (string) $body : $body; // Don't type-cast false-ish values, eg null is null not ''
177
		return $this;
178
	}
179
180
	/**
181
	 * @return string
182
	 */
183
	public function getBody() {
184
		return $this->body;
185
	}
186
187
	/**
188
	 * Add a HTTP header to the response, replacing any header of the same name.
189
	 *
190
	 * @param string $header Example: "Content-Type"
191
	 * @param string $value Example: "text/xml"
192
	 * @return $this
193
	 */
194
	public function addHeader($header, $value) {
195
		$this->headers[$header] = $value;
196
		return $this;
197
	}
198
199
	/**
200
	 * Return the HTTP header of the given name.
201
	 *
202
	 * @param string $header
203
	 * @returns string
204
	 */
205
	public function getHeader($header) {
206
		if(isset($this->headers[$header])) {
207
			return $this->headers[$header];
208
		}
209
		return null;
210
	}
211
212
	/**
213
	 * @return array
214
	 */
215
	public function getHeaders() {
216
		return $this->headers;
217
	}
218
219
	/**
220
	 * Remove an existing HTTP header by its name,
221
	 * e.g. "Content-Type".
222
	 *
223
	 * @param string $header
224
	 * @return $this
225
	 */
226
	public function removeHeader($header) {
227
		unset($this->headers[$header]);
228
		return $this;
229
	}
230
231
	/**
232
	 * @param string $dest
233
	 * @param int $code
234
	 * @return $this
235
	 */
236
	public function redirect($dest, $code = 302) {
237
		if(!in_array($code, self::$redirect_codes)) {
238
			trigger_error("Invalid HTTP redirect code {$code}", E_USER_WARNING);
239
			$code = 302;
240
		}
241
		$this->setStatusCode($code);
242
		$this->headers['Location'] = $dest;
243
		return $this;
244
	}
245
246
	/**
247
	 * Send this HTTPReponse to the browser
248
	 */
249
	public function output() {
0 ignored issues
show
Coding Style introduced by
output 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...
250
		// Attach appropriate X-Include-JavaScript and X-Include-CSS headers
251
		if(Director::is_ajax()) {
252
			Requirements::include_in_response($this);
253
		}
254
255
		if(in_array($this->statusCode, self::$redirect_codes) && headers_sent($file, $line)) {
256
			$url = Director::absoluteURL($this->headers['Location'], true);
257
			$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 256 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...
258
			$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 256 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...
259
			$title = Director::isDev()
260
				? "{$urlATT}... (output started on {$file}, line {$line})"
261
				: "{$urlATT}...";
262
			echo <<<EOT
263
<p>Redirecting to <a href="{$urlATT}" title="Click this link if your browser does not redirect you">{$title}</a></p>
264
<meta http-equiv="refresh" content="1; url={$urlATT}" />
265
<script type="application/javascript">setTimeout(function(){
266
	window.location.href = "{$urlJS}";
267
}, 50);</script>
268
EOT
269
			;
270
		} else {
271
			$line = $file = null;
272
			if(!headers_sent($file, $line)) {
273
				header($_SERVER['SERVER_PROTOCOL'] . " $this->statusCode " . $this->getStatusDescription());
274
				foreach($this->headers as $header => $value) {
275
					//etags need to be quoted
276
					if (strcasecmp('etag', $header) === 0 && 0 !== strpos($value, '"')) {
277
						$value = sprintf('"%s"', $value);
278
					}
279
					header("$header: $value", true, $this->statusCode);
280
				}
281
			} else {
282
				// It's critical that these status codes are sent; we need to report a failure if not.
283
				if($this->statusCode >= 300) {
284
					user_error(
285
						"Couldn't set response type to $this->statusCode because " .
286
						"of output on line $line of $file",
287
						E_USER_WARNING
288
					);
289
				}
290
			}
291
292
			// Only show error pages or generic "friendly" errors if the status code signifies
293
			// an error, and the response doesn't have any body yet that might contain
294
			// a more specific error description.
295
			if(Director::isLive() && $this->isError() && !$this->body) {
296
				$formatter = Injector::inst()->get('FriendlyErrorFormatter');
297
				echo $formatter->format(array(
298
					'code' => $this->statusCode
299
				));
300
301
			} else {
302
				echo $this->body;
303
			}
304
305
		}
306
	}
307
308
	/**
309
	 * Returns true if this response is "finished", that is, no more script execution should be done.
310
	 * Specifically, returns true if a redirect has already been requested
311
	 *
312
	 * @return bool
313
	 */
314
	public function isFinished() {
315
		return in_array($this->statusCode, array(301, 302, 303, 304, 305, 307, 401, 403));
316
	}
317
318
}
319