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

HTTP::set_cache_age()   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 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace SilverStripe\Control;
4
use Deprecation;
5
use InvalidArgumentException;
6
use Convert;
7
use finfo;
8
use File;
9
use Config;
10
11
use SilverStripe\Control\HTTPResponse;
12
13
14
15
/**
16
 * A class with HTTP-related helpers. Like Debug, this is more a bundle of methods than a class.
17
 *
18
 * @package framework
19
 *
20
 * @subpackage misc
21
 */
22
class HTTP {
23
24
	/**
25
	 * @var int
26
	 */
27
	protected static $cache_age = 0;
28
29
	/**
30
	 * @var int
31
	 */
32
	protected static $modification_date = null;
33
34
	/**
35
	 * @var string
36
	 */
37
	protected static $etag = null;
38
39
	/**
40
	 * @config
41
	 *
42
	 * @var bool
43
	 */
44
	private static $cache_ajax_requests = true;
45
46
	/**
47
	 * Turns a local system filename into a URL by comparing it to the script filename.
48
	 *
49
	 * @param string $filename
50
	 *
51
	 * @return string
52
	 */
53
	public static function filename2url($filename) {
54
		$slashPos = -1;
55
56
		while(($slashPos = strpos($filename, "/", $slashPos+1)) !== false) {
57
			if(substr($filename, 0, $slashPos) == substr($_SERVER['SCRIPT_FILENAME'], 0, $slashPos)) {
58
				$commonLength = $slashPos;
59
			} else {
60
				break;
61
			}
62
		}
63
64
		$urlBase = substr(
65
			$_SERVER['PHP_SELF'],
66
			0,
67
			-(strlen($_SERVER['SCRIPT_FILENAME']) - $commonLength)
0 ignored issues
show
Bug introduced by
The variable $commonLength does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
68
		);
69
70
		$url = $urlBase . substr($filename, $commonLength);
71
		$protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') ? "https" : "http";
72
73
		// Count the number of extra folders the script is in.
74
		// $prefix = str_repeat("../", substr_count(substr($_SERVER[SCRIPT_FILENAME], $commonBaseLength)));
75
76
		return "$protocol://". $_SERVER['HTTP_HOST'] . $url;
77
	}
78
79
	/**
80
	 * Turn all relative URLs in the content to absolute URLs.
81
	 *
82
	 * @param string $html
83
	 *
84
	 * @return string
85
	 */
86
	public static function absoluteURLs($html) {
87
		$html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html);
88
		return HTTP::urlRewriter($html, function($url) {
89
			//no need to rewrite, if uri has a protocol (determined here by existence of reserved URI character ":")
90
			if(preg_match('/^\w+:/', $url)) {
91
				return $url;
92
			}
93
			return Director::absoluteURL($url, 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...
94
		});
95
	}
96
97
	/**
98
	 * Rewrite all the URLs in the given content, evaluating the given string as PHP code.
99
	 *
100
	 * Put $URL where you want the URL to appear, however, you can't embed $URL in strings, for example:
101
	 * <ul>
102
	 * <li><code>'"../../" . $URL'</code></li>
103
	 * <li><code>'myRewriter($URL)'</code></li>
104
	 * <li><code>'(substr($URL, 0, 1)=="/") ? "../" . substr($URL, 1) : $URL'</code></li>
105
	 * </ul>
106
	 *
107
	 * As of 3.2 $code should be a callable which takes a single parameter and returns the rewritten,
108
	 * for example:
109
	 * <code>
110
	 * function($url) {
111
	 *		return Director::absoluteURL($url, true);
112
	 * }
113
	 * </code>
114
	 *
115
	 * @param string $content The HTML to search for links to rewrite.
116
	 * @param string|callable $code Either a string that can evaluate to an expression to rewrite links
117
	 * (depreciated), or a callable that takes a single parameter and returns the rewritten URL.
118
	 *
119
	 * @return The content with all links rewritten as per the logic specified in $code.
120
	 */
121
	public static function urlRewriter($content, $code) {
122
		if(!is_callable($code)) {
123
			Deprecation::notice('4.0', 'HTTP::urlRewriter expects a callable as the second parameter');
124
		}
125
126
		// Replace attributes
127
		$attribs = array("src", "background", "a" => "href", "link" => "href", "base" => "href");
128
		foreach($attribs as $tag => $attrib) {
129
			if(!is_numeric($tag)) $tagPrefix = "$tag ";
130
			else $tagPrefix = "";
131
132
			$regExps[] = "/(<{$tagPrefix}[^>]*$attrib *= *\")([^\"]*)(\")/i";
0 ignored issues
show
Coding Style Comprehensibility introduced by
$regExps was never initialized. Although not strictly required by PHP, it is generally a good practice to add $regExps = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
133
			$regExps[] = "/(<{$tagPrefix}[^>]*$attrib *= *')([^']*)(')/i";
134
			$regExps[] = "/(<{$tagPrefix}[^>]*$attrib *= *)([^\"' ]*)( )/i";
135
		}
136
		// Replace css styles
137
		// @todo - http://www.css3.info/preview/multiple-backgrounds/
138
		$styles = array('background-image', 'background', 'list-style-image', 'list-style', 'content');
139
		foreach($styles as $style) {
140
			$regExps[] = "/($style:[^;]*url *\(\")([^\"]+)(\"\))/i";
0 ignored issues
show
Bug introduced by
The variable $regExps does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
141
			$regExps[] = "/($style:[^;]*url *\(')([^']+)('\))/i";
142
			$regExps[] = "/($style:[^;]*url *\()([^\"\)')]+)(\))/i";
143
		}
144
145
		// Callback for regexp replacement
146
		$callback = function($matches) use($code) {
147
			if(is_callable($code)) {
148
				$rewritten = $code($matches[2]);
149
			} else {
150
				// Expose the $URL variable to be used by the $code expression
151
				$URL = $matches[2];
0 ignored issues
show
Unused Code introduced by
$URL is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
152
				$rewritten = eval("return ($code);");
153
			}
154
			return $matches[1] . $rewritten . $matches[3];
155
		};
156
157
		// Execute each expression
158
		foreach($regExps as $regExp) {
159
			$content = preg_replace_callback($regExp, $callback, $content);
160
		}
161
162
		return $content;
163
	}
164
165
	/**
166
	 * Will try to include a GET parameter for an existing URL, preserving existing parameters and
167
	 * fragments. If no URL is given, falls back to $_SERVER['REQUEST_URI']. Uses parse_url() to
168
	 * dissect the URL, and http_build_query() to reconstruct it with the additional parameter.
169
	 * Converts any '&' (ampersand) URL parameter separators to the more XHTML compliant '&amp;'.
170
	 *
171
	 * CAUTION: If the URL is determined to be relative, it is prepended with Director::absoluteBaseURL().
172
	 * This method will always return an absolute URL because Director::makeRelative() can lead to
173
	 * inconsistent results.
174
	 *
175
	 * @param string $varname
176
	 * @param string $varvalue
177
	 * @param string $currentURL Relative or absolute URL.
178
	 * @param string $separator Separator for http_build_query().
179
	 *
180
	 * @return string
181
	 */
182
	public static function setGetVar($varname, $varvalue, $currentURL = null, $separator = '&amp;') {
183
		$uri = $currentURL ? $currentURL : Director::makeRelative($_SERVER['REQUEST_URI']);
184
185
		$isRelative = false;
186
		// We need absolute URLs for parse_url()
187
		if(Director::is_relative_url($uri)) {
188
			$uri = Director::absoluteBaseURL() . $uri;
189
			$isRelative = true;
190
		}
191
192
		// try to parse uri
193
		$parts = parse_url($uri);
194
		if(!$parts) {
195
			throw new InvalidArgumentException("Can't parse URL: " . $uri);
196
		}
197
198
		// Parse params and add new variable
199
		$params = array();
200
		if(isset($parts['query'])) parse_str($parts['query'], $params);
201
		$params[$varname] = $varvalue;
202
203
		// Generate URI segments and formatting
204
		$scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http';
205
		$user = (isset($parts['user']) && $parts['user'] != '')  ? $parts['user'] : '';
206
207
		if($user != '') {
208
			// format in either user:[email protected] or [email protected]
209
			$user .= (isset($parts['pass']) && $parts['pass'] != '') ? ':' . $parts['pass'] . '@' : '@';
210
		}
211
212
		$host = (isset($parts['host'])) ? $parts['host'] : '';
213
		$port = (isset($parts['port']) && $parts['port'] != '') ? ':'.$parts['port'] : '';
214
		$path = (isset($parts['path']) && $parts['path'] != '') ? $parts['path'] : '';
215
216
		// handle URL params which are existing / new
217
		$params = ($params) ?  '?' . http_build_query($params, null, $separator) : '';
218
219
		// keep fragments (anchors) intact.
220
		$fragment = (isset($parts['fragment']) && $parts['fragment'] != '') ?  '#'.$parts['fragment'] : '';
221
222
		// Recompile URI segments
223
		$newUri =  $scheme . '://' . $user . $host . $port . $path . $params . $fragment;
224
225
		if($isRelative) return Director::makeRelative($newUri);
226
227
		return $newUri;
228
	}
229
230
	/**
231
	 * @param string $varname
232
	 * @param string $varvalue
233
	 * @param null|string $currentURL
234
	 *
235
	 * @return string
236
	 */
237
	public static function RAW_setGetVar($varname, $varvalue, $currentURL = null) {
238
		$url = self::setGetVar($varname, $varvalue, $currentURL);
239
		return Convert::xml2raw($url);
240
	}
241
242
	/**
243
	 * Search for all tags with a specific attribute, then return the value of that attribute in a
244
	 * flat array.
245
	 *
246
	 * @param string $content
247
	 * @param array $attributes An array of tags to attributes, for example "[a] => 'href', [div] => 'id'"
248
	 *
249
	 * @return array
250
	 */
251
	public static function findByTagAndAttribute($content, $attributes) {
252
		$regexes = array();
253
254
		foreach($attributes as $tag => $attribute) {
255
			$regexes[] = "/<{$tag} [^>]*$attribute *= *([\"'])(.*?)\\1[^>]*>/i";
256
			$regexes[] = "/<{$tag} [^>]*$attribute *= *([^ \"'>]+)/i";
257
		}
258
259
		$result = array();
260
261
		if($regexes) foreach($regexes as $regex) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $regexes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
262
			if(preg_match_all($regex, $content, $matches)) {
263
				$result = array_merge_recursive($result, (isset($matches[2]) ? $matches[2] : $matches[1]));
264
			}
265
		}
266
267
		return count($result) ? $result : null;
268
	}
269
270
	/**
271
	 * @param string $content
272
	 *
273
	 * @return array
274
	 */
275
	public static function getLinksIn($content) {
276
		return self::findByTagAndAttribute($content, array("a" => "href"));
277
	}
278
279
	/**
280
	 * @param string $content
281
	 *
282
	 * @return array
283
	 */
284
	public static function getImagesIn($content) {
285
		return self::findByTagAndAttribute($content, array("img" => "src"));
286
	}
287
288
	/**
289
	 * Get the MIME type based on a file's extension. If the finfo class exists in PHP, and the file
290
	 * exists relative to the project root, then use that extension, otherwise fallback to a list of
291
	 * commonly known MIME types.
292
	 *
293
	 * @param string $filename
294
	 *
295
	 * @return string
296
	 */
297
	public static function get_mime_type($filename) {
298
		// If the finfo module is compiled into PHP, use it.
299
		$path = BASE_PATH . DIRECTORY_SEPARATOR . $filename;
300
		if(class_exists('finfo') && file_exists($path)) {
301
			$finfo = new finfo(FILEINFO_MIME_TYPE);
302
			return $finfo->file($path);
303
		}
304
305
		// Fallback to use the list from the HTTP.yml configuration and rely on the file extension
306
		// to get the file mime-type
307
		$ext = File::get_file_extension($filename);
308
		// Get the mime-types
309
		$mimeTypes = Config::inst()->get('SilverStripe\Control\HTTP', 'MimeTypes');
310
311
		// The mime type doesn't exist
312
		if(!isset($mimeTypes[$ext])) {
313
			return 'application/unknown';
314
		}
315
316
		return $mimeTypes[$ext];
317
	}
318
319
	/**
320
	 * Set the maximum age of this page in web caches, in seconds.
321
	 *
322
	 * @param int $age
323
	 */
324
	public static function set_cache_age($age) {
325
		self::$cache_age = $age;
326
	}
327
328
	/**
329
	 * @param string $dateString
330
	 */
331
	public static function register_modification_date($dateString) {
332
		$timestamp = strtotime($dateString);
333
		if($timestamp > self::$modification_date)
334
			self::$modification_date = $timestamp;
335
	}
336
337
	/**
338
	 * @param int $timestamp
339
	 */
340
	public static function register_modification_timestamp($timestamp) {
341
		if($timestamp > self::$modification_date)
342
			self::$modification_date = $timestamp;
343
	}
344
345
	/**
346
	 * @param string $etag
347
	 */
348
	public static function register_etag($etag) {
349
		self::$etag = $etag;
350
	}
351
352
	/**
353
	 * Add the appropriate caching headers to the response, including If-Modified-Since / 304 handling.
354
	 * Note that setting HTTP::$cache_age will overrule any cache headers set by PHP's
355
	 * session_cache_limiter functionality. It is your responsibility to ensure only cacheable data
356
	 * is in fact cached, and HTTP::$cache_age isn't set when the HTTP body contains session-specific
357
	 * content.
358
	 *
359
	 * Omitting the $body argument or passing a string is deprecated; in these cases, the headers are
360
	 * output directly.
361
	 *
362
	 * @param SS_HTTPResponse $body
363
	 */
364
	public static function add_cache_headers($body = null) {
365
		$cacheAge = self::$cache_age;
366
367
		// Validate argument
368
		if($body && !($body instanceof HTTPResponse)) {
369
			user_error("HTTP::add_cache_headers() must be passed an SS_HTTPResponse object", E_USER_WARNING);
370
			$body = null;
371
		}
372
373
		// Development sites have frequently changing templates; this can get stuffed up by the code
374
		// below.
375
		if(Director::isDev()) $cacheAge = 0;
376
377
		// The headers have been sent and we don't have an SS_HTTPResponse object to attach things to; no point in
378
		// us trying.
379
		if(headers_sent() && !$body) return;
380
381
		// Populate $responseHeaders with all the headers that we want to build
382
		$responseHeaders = array();
383
384
		$config = Config::inst();
385
		$cacheControlHeaders = Config::inst()->get('SilverStripe\Control\HTTP', 'cache_control');
386
387
388
		// currently using a config setting to cancel this, seems to be so that the CMS caches ajax requests
389
		if(function_exists('apache_request_headers') && $config->get(get_called_class(), 'cache_ajax_requests')) {
390
			$requestHeaders = array_change_key_case(apache_request_headers(), CASE_LOWER);
391
392
			if(isset($requestHeaders['x-requested-with']) && $requestHeaders['x-requested-with']=='XMLHttpRequest') {
393
				$cacheAge = 0;
394
			}
395
		}
396
397
		if($cacheAge > 0) {
398
			$cacheControlHeaders['max-age'] = self::$cache_age;
399
400
			// Set empty pragma to avoid PHP's session_cache_limiter adding conflicting caching information,
401
			// defaulting to "nocache" on most PHP configurations (see http://php.net/session_cache_limiter).
402
			// Since it's a deprecated HTTP 1.0 option, all modern HTTP clients and proxies should
403
			// prefer the caching information indicated through the "Cache-Control" header.
404
			$responseHeaders["Pragma"] = "";
405
406
			// To do: User-Agent should only be added in situations where you *are* actually
407
			// varying according to user-agent.
408
			$vary = $config->get('SilverStripe\Control\HTTP', 'vary');
409
			if ($vary && strlen($vary)) {
410
				$responseHeaders['Vary'] = $vary;
411
			}
412
		}
413
		else {
414
			if($body) {
415
				// Grab header for checking. Unfortunately HTTPRequest uses a mistyped variant.
416
				$contentDisposition = $body->getHeader('Content-disposition');
417
				if (!$contentDisposition) $contentDisposition = $body->getHeader('Content-Disposition');
418
			}
419
420
			if(
421
				$body &&
422
				Director::is_https() &&
423
				isset($_SERVER['HTTP_USER_AGENT']) &&
424
				strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')==true &&
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE') of type string to the boolean true. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
425
				strstr($contentDisposition, 'attachment;')==true
0 ignored issues
show
Bug introduced by
The variable $contentDisposition does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing strstr($contentDisposition, 'attachment;') of type string to the boolean true. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
426
			) {
427
				// IE6-IE8 have problems saving files when https and no-cache are used
428
				// (http://support.microsoft.com/kb/323308)
429
				// Note: this is also fixable by ticking "Do not save encrypted pages to disk" in advanced options.
430
				$cacheControlHeaders['max-age'] = 3;
431
432
				// Set empty pragma to avoid PHP's session_cache_limiter adding conflicting caching information,
433
				// defaulting to "nocache" on most PHP configurations (see http://php.net/session_cache_limiter).
434
				// Since it's a deprecated HTTP 1.0 option, all modern HTTP clients and proxies should
435
				// prefer the caching information indicated through the "Cache-Control" header.
436
				$responseHeaders["Pragma"] = "";
437
			} else {
438
				$cacheControlHeaders['no-cache'] = "true";
439
				$cacheControlHeaders['no-store'] = "true";
440
			}
441
		}
442
443
		foreach($cacheControlHeaders as $header => $value) {
0 ignored issues
show
Bug introduced by
The expression $cacheControlHeaders of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
444
			if(is_null($value)) {
445
				unset($cacheControlHeaders[$header]);
446
			} elseif((is_bool($value) && $value) || $value === "true") {
447
				$cacheControlHeaders[$header] = $header;
448
			} else {
449
				$cacheControlHeaders[$header] = $header."=".$value;
450
			}
451
		}
452
453
		$responseHeaders['Cache-Control'] = implode(', ', $cacheControlHeaders);
454
		unset($cacheControlHeaders, $header, $value);
455
456
		if(self::$modification_date && $cacheAge > 0) {
457
			$responseHeaders["Last-Modified"] = self::gmt_date(self::$modification_date);
458
459
			// Chrome ignores Varies when redirecting back (http://code.google.com/p/chromium/issues/detail?id=79758)
460
			// which means that if you log out, you get redirected back to a page which Chrome then checks against
461
			// last-modified (which passes, getting a 304)
462
			// when it shouldn't be trying to use that page at all because it's the "logged in" version.
463
			// By also using and etag that includes both the modification date and all the varies
464
			// values which we also check against we can catch this and not return a 304
465
			$etagParts = array(self::$modification_date, serialize($_COOKIE));
466
			$etagParts[] = Director::is_https() ? 'https' : 'http';
467
			if (isset($_SERVER['HTTP_USER_AGENT'])) $etagParts[] = $_SERVER['HTTP_USER_AGENT'];
468
			if (isset($_SERVER['HTTP_ACCEPT'])) $etagParts[] = $_SERVER['HTTP_ACCEPT'];
469
470
			$etag = sha1(implode(':', $etagParts));
471
			$responseHeaders["ETag"] = $etag;
472
473
			// 304 response detection
474
			if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
475
				$ifModifiedSince = strtotime(stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']));
476
477
				// As above, only 304 if the last request had all the same varies values
478
				// (or the etag isn't passed as part of the request - but with chrome it always is)
479
				$matchesEtag = !isset($_SERVER['HTTP_IF_NONE_MATCH']) || $_SERVER['HTTP_IF_NONE_MATCH'] == $etag;
480
481
				if($ifModifiedSince >= self::$modification_date && $matchesEtag) {
482
					if($body) {
483
						$body->setStatusCode(304);
484
						$body->setBody('');
485
					} else {
486
						header('HTTP/1.0 304 Not Modified');
487
						die();
488
					}
489
				}
490
			}
491
492
			$expires = time() + $cacheAge;
493
			$responseHeaders["Expires"] = self::gmt_date($expires);
494
		}
495
496
		if(self::$etag) {
497
			$responseHeaders['ETag'] = self::$etag;
498
		}
499
500
		// Now that we've generated them, either output them or attach them to the SS_HTTPResponse as appropriate
501
		foreach($responseHeaders as $k => $v) {
502
			if($body) {
503
				// Set the header now if it's not already set.
504
				if ($body->getHeader($k) === null) {
505
					$body->addHeader($k, $v);
506
				}
507
			} elseif(!headers_sent()) {
508
				header("$k: $v");
509
			}
510
		}
511
	}
512
513
514
	/**
515
	 * Return an {@link http://www.faqs.org/rfcs/rfc2822 RFC 2822} date in the GMT timezone (a timestamp
516
	 * is always in GMT: the number of seconds since January 1 1970 00:00:00 GMT)
517
	 *
518
	 * @param int $timestamp
519
	 *
520
	 * @return string
521
	 */
522
	public static function gmt_date($timestamp) {
523
		return gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
524
	}
525
526
	/**
527
	 * Return static variable cache_age in second
528
	 *
529
	 * @return int
530
	 */
531
	public static function get_cache_age() {
532
		return self::$cache_age;
533
	}
534
535
}
536
537
538