Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

HTTP::urlRewriter()   C

Complexity

Conditions 7
Paths 24

Size

Total Lines 43
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 25
nc 24
nop 2
dl 0
loc 43
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * A class with HTTP-related helpers.
5
 * Like Debug, this is more a bundle of methods than a class ;-)
6
 *
7
 * @package framework
8
 * @subpackage misc
9
 */
10
class HTTP {
11
12
	/**
13
	 * @var int $cache_age
14
	 */
15
	protected static $cache_age = 0;
16
17
	/**
18
	 * @var timestamp $modification_date
19
	 */
20
	protected static $modification_date = null;
21
22
	/**
23
	 * @var string $etag
24
	 */
25
	protected static $etag = null;
26
27
	/**
28
	 * @config
29
	 */
30
	private static $cache_ajax_requests = true;
31
32
	/**
33
	 * Turns a local system filename into a URL by comparing it to the script
34
	 * filename.
35
	 *
36
	 * @param string
37
	 */
38
	public static function filename2url($filename) {
39
		$slashPos = -1;
40
41
		while(($slashPos = strpos($filename, "/", $slashPos+1)) !== false) {
42
			if(substr($filename, 0, $slashPos) == substr($_SERVER['SCRIPT_FILENAME'],0,$slashPos)) {
43
				$commonLength = $slashPos;
44
			} else {
45
				break;
46
			}
47
		}
48
49
		$urlBase = substr(
50
			$_SERVER['PHP_SELF'],
51
			0,
52
			-(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...
53
		);
54
55
		$url = $urlBase . substr($filename, $commonLength);
56
		$protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') ? "https" : "http";
57
58
		// Count the number of extra folders the script is in.
59
		// $prefix = str_repeat("../", substr_count(substr($_SERVER[SCRIPT_FILENAME],$commonBaseLength)));
60
61
		return "$protocol://". $_SERVER['HTTP_HOST'] . $url;
62
	}
63
64
	/**
65
	 * Turn all relative URLs in the content to absolute URLs
66
	 */
67
	public static function absoluteURLs($html) {
68
		$html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html);
69
		return HTTP::urlRewriter($html, function($url) {
70
			//no need to rewrite, if uri has a protocol (determined here by existence of reserved URI character ":")
71
			if(preg_match('/^\w+:/', $url)){
72
				return $url;
73
			}
74
			return Director::absoluteURL($url, true);
75
		});
76
	}
77
78
	/**
79
	 * Rewrite all the URLs in the given content, evaluating the given string as PHP code.
80
	 *
81
	 * Put $URL where you want the URL to appear, however, you can't embed $URL in strings
82
	 * Some example code:
83
	 * <ul>
84
	 * <li><code>'"../../" . $URL'</code></li>
85
	 * <li><code>'myRewriter($URL)'</code></li>
86
	 * <li><code>'(substr($URL,0,1)=="/") ? "../" . substr($URL,1) : $URL'</code></li>
87
	 * </ul>
88
	 *
89
	 * As of 3.2 $code should be a callable which takes a single parameter and returns
90
	 * the rewritten URL. e.g.
91
	 *
92
	 * <code>
93
	 * function($url) {
94
	 *		return Director::absoluteURL($url, true);
95
	 * }
96
	 * </code>
97
	 *
98
	 * @param string $content The HTML to search for links to rewrite
99
	 * @param string|callable $code Either a string that can evaluate to an expression
100
	 * to rewrite links (depreciated), or a callable that takes a single
101
	 * parameter and returns the rewritten URL
102
	 * @return The content with all links rewritten as per the logic specified in $code
103
	 */
104
	public static function urlRewriter($content, $code) {
105
		if(!is_callable($code)) {
106
			Deprecation::notice('4.0', 'HTTP::urlRewriter expects a callable as the second parameter');
107
		}
108
109
		// Replace attributes
110
		$attribs = array("src","background","a" => "href","link" => "href", "base" => "href");
111
		foreach($attribs as $tag => $attrib) {
112
			if(!is_numeric($tag)) $tagPrefix = "$tag ";
113
			else $tagPrefix = "";
114
115
			$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...
116
			$regExps[] = "/(<{$tagPrefix}[^>]*$attrib *= *')([^']*)(')/i";
117
			$regExps[] = "/(<{$tagPrefix}[^>]*$attrib *= *)([^\"' ]*)( )/i";
118
		}
119
		// Replace css styles
120
		// @todo - http://www.css3.info/preview/multiple-backgrounds/
121
		$styles = array('background-image', 'background', 'list-style-image', 'list-style', 'content');
122
		foreach($styles as $style) {
123
			$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...
124
			$regExps[] = "/($style:[^;]*url *\(')([^']+)('\))/i";
125
			$regExps[] = "/($style:[^;]*url *\()([^\"\)')]+)(\))/i";
126
		}
127
128
		// Callback for regexp replacement
129
		$callback = function($matches) use($code) {
130
			if(is_callable($code)) {
131
				$rewritten = $code($matches[2]);
132
			} else {
133
				// Expose the $URL variable to be used by the $code expression
134
				$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...
135
				$rewritten = eval("return ($code);");
136
			}
137
			return $matches[1] . $rewritten . $matches[3];
138
		};
139
140
		// Execute each expression
141
		foreach($regExps as $regExp) {
142
			$content = preg_replace_callback($regExp, $callback, $content);
143
		}
144
145
		return $content;
146
	}
147
148
	/**
149
	 * Will try to include a GET parameter for an existing URL,
150
	 * preserving existing parameters and fragments.
151
	 * If no URL is given, falls back to $_SERVER['REQUEST_URI'].
152
	 * Uses parse_url() to dissect the URL, and http_build_query() to reconstruct it
153
	 * with the additional parameter. Converts any '&' (ampersand)
154
	 * URL parameter separators to the more XHTML compliant '&amp;'.
155
	 *
156
	 * CAUTION: If the URL is determined to be relative,
157
	 * it is prepended with Director::absoluteBaseURL().
158
	 * This method will always return an absolute URL because
159
	 * Director::makeRelative() can lead to inconsistent results.
160
	 *
161
	 * @param String $varname
162
	 * @param String $varvalue
163
	 * @param String $currentURL Relative or absolute URL (Optional).
164
	 * @param String $separator Separator for http_build_query(). (Optional).
165
	 * @return String Absolute URL
166
	 */
167
	public static function setGetVar($varname, $varvalue, $currentURL = null, $separator = '&amp;') {
168
		$uri = $currentURL ? $currentURL : Director::makeRelative($_SERVER['REQUEST_URI']);
169
170
		$isRelative = false;
171
		// We need absolute URLs for parse_url()
172
		if(Director::is_relative_url($uri)) {
173
			$uri = Director::absoluteBaseURL() . $uri;
174
			$isRelative = true;
175
		}
176
177
		// try to parse uri
178
		$parts = parse_url($uri);
179
		if(!$parts) {
180
			throw new InvalidArgumentException("Can't parse URL: " . $uri);
181
		}
182
183
		// Parse params and add new variable
184
		$params = array();
185
		if(isset($parts['query'])) parse_str($parts['query'], $params);
186
		$params[$varname] = $varvalue;
187
188
		// Generate URI segments and formatting
189
		$scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http';
190
		$user = (isset($parts['user']) && $parts['user'] != '')  ? $parts['user'] : '';
191
192
		if($user != '') {
193
			// format in either user:[email protected] or [email protected]
194
			$user .= (isset($parts['pass']) && $parts['pass'] != '') ? ':' . $parts['pass'] . '@' : '@';
195
		}
196
197
		$host = (isset($parts['host'])) ? $parts['host'] : '';
198
		$port = (isset($parts['port']) && $parts['port'] != '') ? ':'.$parts['port'] : '';
199
		$path = (isset($parts['path']) && $parts['path'] != '') ? $parts['path'] : '';
200
201
		// handle URL params which are existing / new
202
		$params = ($params) ?  '?' . http_build_query($params, null, $separator) : '';
203
204
		// keep fragments (anchors) intact.
205
		$fragment = (isset($parts['fragment']) && $parts['fragment'] != '') ?  '#'.$parts['fragment'] : '';
206
207
		// Recompile URI segments
208
		$newUri =  $scheme . '://' . $user . $host . $port . $path . $params . $fragment;
209
210
		if($isRelative) return Director::makeRelative($newUri);
211
212
		return $newUri;
213
	}
214
215
	public static function RAW_setGetVar($varname, $varvalue, $currentURL = null) {
216
		$url = self::setGetVar($varname, $varvalue, $currentURL);
217
		return Convert::xml2raw($url);
218
	}
219
220
	/**
221
	 * Search for all tags with a specific attribute, then return the value of that attribute in a flat array.
222
	 *
223
	 * @param string $content
224
	 * @param array $attributes an array of tags to attributes, for example "[a] => 'href', [div] => 'id'"
225
	 * @return array
226
	 */
227
	public static function findByTagAndAttribute($content, $attributes) {
228
		$regexes = array();
229
230
		foreach($attributes as $tag => $attribute) {
231
			$regexes[] = "/<{$tag} [^>]*$attribute *= *([\"'])(.*?)\\1[^>]*>/i";
232
			$regexes[] = "/<{$tag} [^>]*$attribute *= *([^ \"'>]+)/i";
233
		}
234
235
		$result = array();
236
237
		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...
238
			if(preg_match_all($regex, $content, $matches)) {
239
				$result = array_merge_recursive($result, (isset($matches[2]) ? $matches[2] : $matches[1]));
240
			}
241
		}
242
243
		return count($result) ? $result : null;
244
	}
245
246
	public static function getLinksIn($content) {
247
		return self::findByTagAndAttribute($content, array("a" => "href"));
248
	}
249
250
	public static function getImagesIn($content) {
251
		return self::findByTagAndAttribute($content, array("img" => "src"));
252
	}
253
254
	/**
255
	 * Get the MIME type based on a file's extension.
256
	 *
257
	 * If the finfo class exists in PHP, and the file actually exists, then use that
258
	 * extension, otherwise fallback to a list of commonly known MIME types.
259
	 *
260
	 * @uses finfo
261
	 * @param string $filename Relative path to filename from project root, e.g. "mysite/tests/file.csv"
262
	 * @return string MIME type
263
	 */
264
	public static function get_mime_type($filename) {
265
		// If the finfo module is compiled into PHP, use it.
266
		$path = BASE_PATH . DIRECTORY_SEPARATOR . $filename;
267
		if(class_exists('finfo') && file_exists($path)) {
268
			$finfo = new finfo(FILEINFO_MIME_TYPE);
269
			return $finfo->file($path);
270
		}
271
272
		// Fallback to use the list from the HTTP.yml configuration and rely on the file extension
273
		// to get the file mime-type
274
		$ext = File::get_file_extension($filename);
275
		// Get the mime-types
276
		$mimeTypes = Config::inst()->get('HTTP', 'MimeTypes');
277
278
		// The mime type doesn't exist
279
		if(!isset($mimeTypes[$ext])) {
280
			return 'application/unknown';
281
		}
282
283
		return $mimeTypes[$ext];
284
	}
285
286
	/**
287
	 * Set the maximum age of this page in web caches, in seconds
288
	 */
289
	public static function set_cache_age($age) {
290
		self::$cache_age = $age;
291
	}
292
293
	public static function register_modification_date($dateString) {
294
		$timestamp = strtotime($dateString);
295
		if($timestamp > self::$modification_date)
296
			self::$modification_date = $timestamp;
0 ignored issues
show
Documentation Bug introduced by
It seems like $timestamp of type integer is incompatible with the declared type object<timestamp> of property $modification_date.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
297
	}
298
299
	public static function register_modification_timestamp($timestamp) {
300
		if($timestamp > self::$modification_date)
301
			self::$modification_date = $timestamp;
302
	}
303
304
	public static function register_etag($etag) {
305
		self::$etag = $etag;
306
	}
307
308
	/**
309
	 * Add the appropriate caching headers to the response, including If-Modified-Since / 304 handling.
310
	 * Note that setting HTTP::$cache_age will overrule any cache headers set by PHP's
311
	 * session_cache_limiter functionality. It is your responsibility to ensure only cacheable data
312
	 * is in fact cached, and HTTP::$cache_age isn't set when the HTTP body contains session-specific content.
313
	 *
314
	 * @param SS_HTTPResponse $body The SS_HTTPResponse object to augment.  Omitted the argument or passing a string is
315
	 *                            deprecated; in these cases, the headers are output directly.
316
	 */
317
	public static function add_cache_headers($body = null) {
318
		$cacheAge = self::$cache_age;
319
320
		// Validate argument
321
		if($body && !($body instanceof SS_HTTPResponse)) {
322
			user_error("HTTP::add_cache_headers() must be passed an SS_HTTPResponse object", E_USER_WARNING);
323
			$body = null;
324
		}
325
326
		// Development sites have frequently changing templates; this can get stuffed up by the code
327
		// below.
328
		if(Director::isDev()) $cacheAge = 0;
329
330
		// The headers have been sent and we don't have an SS_HTTPResponse object to attach things to; no point in
331
		// us trying.
332
		if(headers_sent() && !$body) return;
333
334
		// Populate $responseHeaders with all the headers that we want to build
335
		$responseHeaders = array();
336
337
		$config = Config::inst();
338
		$cacheControlHeaders = Config::inst()->get('HTTP', 'cache_control');
339
340
341
		// currently using a config setting to cancel this, seems to be so that the CMS caches ajax requests
342
		if(function_exists('apache_request_headers') && $config->get(get_called_class(), 'cache_ajax_requests')) {
343
			$requestHeaders = array_change_key_case(apache_request_headers(), CASE_LOWER);
344
345
			if(isset($requestHeaders['x-requested-with']) && $requestHeaders['x-requested-with']=='XMLHttpRequest') {
346
				$cacheAge = 0;
347
			}
348
		}
349
350
		if($cacheAge > 0) {
351
			$cacheControlHeaders['max-age'] = self::$cache_age;
352
353
			// Set empty pragma to avoid PHP's session_cache_limiter adding conflicting caching information,
354
			// defaulting to "nocache" on most PHP configurations (see http://php.net/session_cache_limiter).
355
			// Since it's a deprecated HTTP 1.0 option, all modern HTTP clients and proxies should
356
			// prefer the caching information indicated through the "Cache-Control" header.
357
			$responseHeaders["Pragma"] = "";
358
359
			// To do: User-Agent should only be added in situations where you *are* actually
360
			// varying according to user-agent.
361
			$vary = $config->get('HTTP', 'vary');
362
			if ($vary && strlen($vary)) {
363
				$responseHeaders['Vary'] = $vary;
364
			}
365
		}
366
		else {
367
			if($body) {
368
				// Grab header for checking. Unfortunately HTTPRequest uses a mistyped variant.
369
				$contentDisposition = $body->getHeader('Content-disposition');
370
				if (!$contentDisposition) $contentDisposition = $body->getHeader('Content-Disposition');
371
			}
372
373
			if(
374
				$body &&
375
				Director::is_https() &&
376
				isset($_SERVER['HTTP_USER_AGENT']) &&
377
				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...
378
				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...
379
			) {
380
				// IE6-IE8 have problems saving files when https and no-cache are used
381
				// (http://support.microsoft.com/kb/323308)
382
				// Note: this is also fixable by ticking "Do not save encrypted pages to disk" in advanced options.
383
				$cacheControlHeaders['max-age'] = 3;
384
385
				// Set empty pragma to avoid PHP's session_cache_limiter adding conflicting caching information,
386
				// defaulting to "nocache" on most PHP configurations (see http://php.net/session_cache_limiter).
387
				// Since it's a deprecated HTTP 1.0 option, all modern HTTP clients and proxies should
388
				// prefer the caching information indicated through the "Cache-Control" header.
389
				$responseHeaders["Pragma"] = "";
390
			} else {
391
				$cacheControlHeaders['no-cache'] = "true";
392
				$cacheControlHeaders['no-store'] = "true";
393
			}
394
		}
395
396
		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...
397
			if(is_null($value)) {
398
				unset($cacheControlHeaders[$header]);
399
			} elseif((is_bool($value) && $value) || $value === "true") {
400
				$cacheControlHeaders[$header] = $header;
401
			} else {
402
				$cacheControlHeaders[$header] = $header."=".$value;
403
			}
404
		}
405
406
		$responseHeaders['Cache-Control'] = implode(', ', $cacheControlHeaders);
407
		unset($cacheControlHeaders, $header, $value);
408
409
		if(self::$modification_date && $cacheAge > 0) {
410
			$responseHeaders["Last-Modified"] = self::gmt_date(self::$modification_date);
411
412
			// Chrome ignores Varies when redirecting back (http://code.google.com/p/chromium/issues/detail?id=79758)
413
			// which means that if you log out, you get redirected back to a page which Chrome then checks against
414
			// last-modified (which passes, getting a 304)
415
			// when it shouldn't be trying to use that page at all because it's the "logged in" version.
416
			// By also using and etag that includes both the modification date and all the varies
417
			// values which we also check against we can catch this and not return a 304
418
			$etagParts = array(self::$modification_date, serialize($_COOKIE));
419
			$etagParts[] = Director::is_https() ? 'https' : 'http';
420
			if (isset($_SERVER['HTTP_USER_AGENT'])) $etagParts[] = $_SERVER['HTTP_USER_AGENT'];
421
			if (isset($_SERVER['HTTP_ACCEPT'])) $etagParts[] = $_SERVER['HTTP_ACCEPT'];
422
423
			$etag = sha1(implode(':', $etagParts));
424
			$responseHeaders["ETag"] = $etag;
425
426
			// 304 response detection
427
			if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
428
				$ifModifiedSince = strtotime(stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']));
429
430
				// As above, only 304 if the last request had all the same varies values
431
				// (or the etag isn't passed as part of the request - but with chrome it always is)
432
				$matchesEtag = !isset($_SERVER['HTTP_IF_NONE_MATCH']) || $_SERVER['HTTP_IF_NONE_MATCH'] == $etag;
433
434
				if($ifModifiedSince >= self::$modification_date && $matchesEtag) {
435
					if($body) {
436
						$body->setStatusCode(304);
437
						$body->setBody('');
438
					} else {
439
						header('HTTP/1.0 304 Not Modified');
440
						die();
441
					}
442
				}
443
			}
444
445
			$expires = time() + $cacheAge;
446
			$responseHeaders["Expires"] = self::gmt_date($expires);
447
		}
448
449
		if(self::$etag) {
450
			$responseHeaders['ETag'] = self::$etag;
451
		}
452
453
		// Now that we've generated them, either output them or attach them to the SS_HTTPResponse as appropriate
454
		foreach($responseHeaders as $k => $v) {
455
			if($body) {
456
				// Set the header now if it's not already set.
457
				if ($body->getHeader($k) === null) {
458
					$body->addHeader($k, $v);
459
				}
460
			} elseif(!headers_sent()) {
461
				header("$k: $v");
462
			}
463
		}
464
	}
465
466
467
	/**
468
	 * Return an {@link http://www.faqs.org/rfcs/rfc2822 RFC 2822} date in the
469
	 * GMT timezone (a timestamp is always in GMT: the number of seconds
470
	 * since January 1 1970 00:00:00 GMT)
471
	 */
472
	public static function gmt_date($timestamp) {
473
		return gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
474
	}
475
476
	/*
477
	 * Return static variable cache_age in second
478
	 */
479
	public static function get_cache_age() {
480
		return self::$cache_age;
481
	}
482
483
}
484
485
486