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

ContentNegotiator::disable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 3
c 1
b 1
f 0
nc 1
nop 0
dl 0
loc 4
rs 10
1
<?php
2
3
namespace SilverStripe\Control;
4
use Deprecation;
5
use Config;
6
7
use Object;
8
use SilverStripe\Control\HTTPResponse;
9
10
11
12
/**
13
 * The content negotiator performs "text/html" or "application/xhtml+xml" switching. It does this through
14
 * the public static function ContentNegotiator::process(). By default, ContentNegotiator will comply to
15
 * the Accept headers the clients sends along with the HTTP request, which is most likely
16
 * "application/xhtml+xml" (see "Order of selection" below).
17
 *
18
 * Order of selection between html or xhtml is as follows:
19
 * - if PHP has already sent the HTTP headers, default to "html" (we can't send HTTP Content-Type headers
20
 *   any longer)
21
 * - if a GET variable ?forceFormat is set, it takes precedence (for testing purposes)
22
 * - if the user agent is detected as W3C Validator we always deliver "xhtml"
23
 * - if an HTTP Accept header is sent from the client, we respect its order (this is the most common case)
24
 * - if none of the above matches, fallback is "html"
25
 *
26
 * ContentNegotiator doesn't enable you to send content as a true XML document through the "text/xml"
27
 * or "application/xhtml+xml" Content-Type.
28
 *
29
 * Please see http://webkit.org/blog/68/understanding-html-xml-and-xhtml/ for further information.
30
 *
31
 * @package framework
32
 *
33
 * @subpackage control
34
 *
35
 * @todo Check for correct XHTML doctype in xhtml()
36
 * @todo Allow for other HTML4 doctypes (e.g. Transitional) in html()
37
 * @todo Make content replacement and doctype setting two separately configurable behaviours
38
 *
39
 * Some developers might know what they're doing and don't want ContentNegotiator messing with their
40
 * HTML4 doctypes, but still find it useful to have self-closing tags removed.
41
 */
42
class ContentNegotiator extends Object {
43
44
	/**
45
	 * @config
46
	 *
47
	 * @var string
48
	 */
49
	private static $content_type = '';
50
51
	/**
52
	 * @config
53
	 *
54
	 * @var string
55
	 */
56
	private static $encoding = 'utf-8';
57
58
	/**
59
	 * @config
60
	 *
61
	 * @var bool
62
	 */
63
	private static $enabled = false;
64
65
	/**
66
	 * @config
67
	 *
68
	 * @var string
69
	 */
70
	private static $default_format = 'html';
71
72
	/**
73
	 * Set the character set encoding for this page. By default it's utf-8, but you could change it to,
74
	 * say, windows-1252, to improve interoperability with extended characters being imported from windows
75
	 * excel.
76
	 *
77
	 * @deprecated 4.0 Use the "ContentNegotiator.encoding" config setting instead
78
	 *
79
	 * @param string $encoding
80
	 */
81
	public static function set_encoding($encoding) {
82
		Deprecation::notice('4.0', 'Use the "ContentNegotiator.encoding" config setting instead');
83
		Config::inst()->update('SilverStripe\Control\ContentNegotiator', 'encoding', $encoding);
84
	}
85
86
	/**
87
	 * Return the character encoding set bhy ContentNegotiator::set_encoding(). It's recommended that all
88
	 * classes that need to specify the character set make use of this function.
89
	 *
90
	 * @deprecated 4.0 Use the "ContentNegotiator.encoding" config setting instead.
91
	 *
92
	 * @return string
93
	 */
94
	public static function get_encoding() {
95
		Deprecation::notice('4.0', 'Use the "ContentNegotiator.encoding" config setting instead');
96
		return Config::inst()->get('SilverStripe\Control\ContentNegotiator', 'encoding');
97
	}
98
99
	/**
100
	 * Enable content negotiation for all templates, not just those with the xml header.
101
	 *
102
	 * @deprecated 4.0 Use the "ContentNegotiator.enabled" config setting instead
103
	 */
104
	public static function enable() {
105
		Deprecation::notice('4.0', 'Use the "ContentNegotiator.enabled" config setting instead');
106
		Config::inst()->update('SilverStripe\Control\ContentNegotiator', 'enabled', true);
107
	}
108
109
	/**
110
	 * Disable content negotiation for all templates, not just those with the xml header.
111
	 *
112
	 * @deprecated 4.0 Use the "ContentNegotiator.enabled" config setting instead
113
	 */
114
	public static function disable() {
115
		Deprecation::notice('4.0', 'Use the "ContentNegotiator.enabled" config setting instead');
116
		Config::inst()->update('SilverStripe\Control\ContentNegotiator', 'enabled', false);
117
	}
118
119
	/**
120
	 * Returns true if negotiation is enabled for the given response. By default, negotiation is only
121
	 * enabled for pages that have the xml header.
122
	 */
123
	public static function enabled_for($response) {
124
		$contentType = $response->getHeader("Content-Type");
125
126
		// Disable content negotiation for other content types
127
		if($contentType && substr($contentType, 0,9) != 'text/html'
128
				&& substr($contentType, 0,21) != 'application/xhtml+xml') {
129
			return false;
130
		}
131
132
		if(Config::inst()->get('SilverStripe\Control\ContentNegotiator', 'enabled')) return true;
133
		else return (substr($response->getBody(),0,5) == '<' . '?xml');
134
	}
135
136
	/**
137
	 * @param SS_HTTPResponse $response
138
	 */
139
	public static function process(HTTPResponse $response) {
140
		if(!self::enabled_for($response)) return;
141
142
		$mimes = array(
143
			"xhtml" => "application/xhtml+xml",
144
			"html" => "text/html",
145
		);
146
		$q = array();
147
		if(headers_sent()) {
148
			$chosenFormat = Config::inst()->get('SilverStripe\Control\ContentNegotiator', 'default_format');
149
150
		} else if(isset($_GET['forceFormat'])) {
151
			$chosenFormat = $_GET['forceFormat'];
152
153
		} else {
154
			// The W3C validator doesn't send an HTTP_ACCEPT header, but it can support xhtml. We put this
155
			// special case in here so that designers don't get worried that their templates are HTML4.
156
			if(isset($_SERVER['HTTP_USER_AGENT']) && substr($_SERVER['HTTP_USER_AGENT'], 0, 14) == 'W3C_Validator/') {
157
				$chosenFormat = "xhtml";
158
159
			} else {
160
				foreach($mimes as $format => $mime) {
161
					$regExp = '/' . str_replace(array('+','/'),array('\+','\/'), $mime) . '(;q=(\d+\.\d+))?/i';
162
					if (isset($_SERVER['HTTP_ACCEPT']) && preg_match($regExp, $_SERVER['HTTP_ACCEPT'], $matches)) {
163
						$preference = isset($matches[2]) ? $matches[2] : 1;
164
						if(!isset($q[$preference])) $q[$preference] = $format;
165
					}
166
				}
167
168
				if($q) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $q 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...
169
					// Get the preferred format
170
					krsort($q);
171
					$chosenFormat = reset($q);
172
				} else {
173
					$chosenFormat = Config::inst()->get('SilverStripe\Control\ContentNegotiator', 'default_format');
174
				}
175
			}
176
		}
177
178
		$negotiator = new ContentNegotiator();
179
		$negotiator->$chosenFormat( $response );
180
	}
181
182
	/**
183
	 * Check user defined content type and use it, if it's empty use the strict application/xhtml+xml.
184
	 * Replaces a few common tags and entities with their XHTML representations (<br>, <img>, &nbsp;
185
	 * <input>, checked, selected).
186
	 *
187
	 * @param SS_HTTPResponse $response
188
	 *
189
	 * @todo Search for more xhtml replacement
190
	 */
191
	public function xhtml(HTTPResponse $response) {
192
		$content = $response->getBody();
193
		$encoding = Config::inst()->get('SilverStripe\Control\ContentNegotiator', 'encoding');
194
195
		$contentType = Config::inst()->get('SilverStripe\Control\ContentNegotiator', 'content_type');
196 View Code Duplication
		if (empty($contentType)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
197
			$response->addHeader("Content-Type", "application/xhtml+xml; charset=" . $encoding);
198
		} else {
199
			$response->addHeader("Content-Type", $contentType . "; charset=" . $encoding);
200
		}
201
		$response->addHeader("Vary" , "Accept");
202
203
		// Fix base tag
204
		$content = preg_replace('/<base href="([^"]*)"><!--\[if[[^\]*]\] \/><!\[endif\]-->/',
205
			'<base href="$1" />', $content);
206
207
		$content = str_replace('&nbsp;','&#160;', $content);
208
		$content = str_replace('<br>','<br />', $content);
209
		$content = str_replace('<hr>','<hr />', $content);
210
		$content = preg_replace('#(<img[^>]*[^/>])>#i', '\\1/>', $content);
211
		$content = preg_replace('#(<input[^>]*[^/>])>#i', '\\1/>', $content);
212
		$content = preg_replace('#(<param[^>]*[^/>])>#i', '\\1/>', $content);
213
		$content = preg_replace("#(\<option[^>]*[\s]+selected)(?!\s*\=)#si", "$1=\"selected\"$2", $content);
214
		$content = preg_replace("#(\<input[^>]*[\s]+checked)(?!\s*\=)#si", "$1=\"checked\"$2", $content);
215
216
		$response->setBody($content);
217
	}
218
219
	/**
220
	 * Performs the following replacements:
221
	 * - Check user defined content type and use it, if it's empty use the text/html.
222
	 * - If find a XML header replaces it and existing doctypes with HTML4.01 Strict.
223
	 * - Replaces self-closing tags like <img /> with unclosed solitary tags like <img>.
224
	 * - Replaces all occurrences of "application/xhtml+xml" with "text/html" in the template.
225
	 * - Removes "xmlns" attributes and any <?xml> Pragmas.
226
	 *
227
	 * @param SS_HTTPResponse $response
228
	 */
229
	public function html(HTTPResponse $response) {
230
		$encoding = Config::inst()->get('SilverStripe\Control\ContentNegotiator', 'encoding');
231
		$contentType = Config::inst()->get('SilverStripe\Control\ContentNegotiator', 'content_type');
232 View Code Duplication
		if (empty($contentType)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
233
			$response->addHeader("Content-Type", "text/html; charset=" . $encoding);
234
		} else {
235
			$response->addHeader("Content-Type", $contentType . "; charset=" . $encoding);
236
		}
237
		$response->addHeader("Vary", "Accept");
238
239
		$content = $response->getBody();
240
		$hasXMLHeader = (substr($content,0,5) == '<' . '?xml' );
241
242
		// Fix base tag
243
		$content = preg_replace('/<base href="([^"]*)" \/>/',
244
			'<base href="$1"><!--[if lte IE 6]></base><![endif]-->', $content);
245
246
		$content = preg_replace("#<\\?xml[^>]+\\?>\n?#", '', $content);
247
		$content = str_replace(array('/>','xml:lang','application/xhtml+xml'),array('>','lang','text/html'), $content);
248
249
		// Only replace the doctype in templates with the xml header
250
		if($hasXMLHeader) {
251
			$content = preg_replace('/<!DOCTYPE[^>]+>/',
252
				'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
253
				$content);
254
		}
255
		$content = preg_replace('/<html xmlns="[^"]+"/','<html ', $content);
256
257
		$response->setBody($content);
258
	}
259
260
}
261