Completed
Branch Scrutinizer (3da711)
by Josh
03:32
created

Configurator::asConfig()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 40
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 40
ccs 18
cts 18
cp 1
rs 9.3554
c 0
b 0
f 0
cc 5
nc 3
nop 0
crap 5
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2019 The s9e Authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter\Plugins\HTMLElements;
9
10
use InvalidArgumentException;
11
use RuntimeException;
12
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
13
use s9e\TextFormatter\Configurator\Items\Tag;
14
use s9e\TextFormatter\Configurator\Items\UnsafeTemplate;
15
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
16
use s9e\TextFormatter\Configurator\Validators\AttributeName;
17
use s9e\TextFormatter\Configurator\Validators\TagName;
18
use s9e\TextFormatter\Plugins\ConfiguratorBase;
19
20
class Configurator extends ConfiguratorBase
21
{
22
	/**
23
	* @var array 2D array using HTML element names as keys, each value being an associative array
24
	*            using HTML attribute names as keys and their alias as values. A special empty entry
25
	*            is used to store the HTML element's alias
26
	*/
27
	protected $aliases = [];
28
29
	/**
30
	* @var array  Default filter of a few known attributes
31
	*
32
	* It doesn't make much sense to try to declare every known HTML attribute here. Validation is
33
	* not the purpose of this plugin. It does make sense however to declare URL attributes as such,
34
	* so that they are subject to our constraints (disallowed hosts, etc...)
35
	*
36
	* @see scripts/patchHTMLElementConfigurator.php
37
	*/
38
	protected $attributeFilters = [
39
		'action'     => '#url',
40
		'cite'       => '#url',
41
		'data'       => '#url',
42
		'formaction' => '#url',
43
		'href'       => '#url',
44
		'icon'       => '#url',
45
		'longdesc'   => '#url',
46
		'manifest'   => '#url',
47
		'ping'       => '#url',
48
		'poster'     => '#url',
49
		'src'        => '#url'
50
	];
51
52
	/**
53
	* @var array  Hash of allowed HTML elements. Element names are lowercased and used as keys for
54
	*             this array
55
	*/
56
	protected $elements = [];
57
58
	/**
59
	* @var string Namespace prefix of the tags produced by this plugin's parser
60
	*/
61
	protected $prefix = 'html';
62
63
	/**
64
	* {@inheritdoc}
65
	*/
66
	protected $quickMatch = '<';
67
68
	/**
69
	* @var array  Blacklist of elements that are considered unsafe
70
	*/
71
	protected $unsafeElements = [
72
		'base',
73
		'embed',
74
		'frame',
75
		'iframe',
76
		'meta',
77
		'object',
78
		'script'
79
	];
80
81
	/**
82
	* @var array  Blacklist of attributes that are considered unsafe, in addition of any attribute
83
	*             whose name starts with "on" such as "onmouseover"
84
	*/
85
	protected $unsafeAttributes = [
86
		'style',
87
		'target'
88
	];
89
90
	/**
91
	* Alias the HTML attribute of given HTML element to a given attribute name
92
	*
93
	* NOTE: will *not* create the target attribute
94
	*
95
	* @param  string $elName   Name of the HTML element
96
	* @param  string $attrName Name of the HTML attribute
97
	* @param  string $alias    Alias
98
	* @return void
99
	*/
100 2
	public function aliasAttribute($elName, $attrName, $alias)
101
	{
102 2
		$elName   = $this->normalizeElementName($elName);
103 2
		$attrName = $this->normalizeAttributeName($attrName);
104
105 2
		$this->aliases[$elName][$attrName] = AttributeName::normalize($alias);
106
	}
107
108
	/**
109
	* Alias an HTML element to a given tag name
110
	*
111
	* NOTE: will *not* create the target tag
112
	*
113
	* @param  string $elName  Name of the HTML element
114
	* @param  string $tagName Name of the tag
115
	* @return void
116
	*/
117 3
	public function aliasElement($elName, $tagName)
118
	{
119 3
		$elName = $this->normalizeElementName($elName);
120
121 3
		$this->aliases[$elName][''] = TagName::normalize($tagName);
122
	}
123
124
	/**
125
	* Allow an HTML element to be used
126
	*
127
	* @param  string $elName Name of the element
128
	* @return Tag            Tag that represents this element
129
	*/
130 21
	public function allowElement($elName)
131
	{
132 21
		return $this->allowElementWithSafety($elName, false);
133
	}
134
135
	/**
136
	* Allow an unsafe HTML element to be used
137
	*
138
	* @param  string $elName Name of the element
139
	* @return Tag            Tag that represents this element
140
	*/
141 2
	public function allowUnsafeElement($elName)
142
	{
143 2
		return $this->allowElementWithSafety($elName, true);
144
	}
145
146
	/**
147
	* Allow a (potentially unsafe) HTML element to be used
148
	*
149
	* @param  string $elName      Name of the element
150
	* @param  bool   $allowUnsafe Whether to allow unsafe elements
151
	* @return Tag                 Tag that represents this element
152
	*/
153 23
	protected function allowElementWithSafety($elName, $allowUnsafe)
154
	{
155 23
		$elName  = $this->normalizeElementName($elName);
156 22
		$tagName = $this->prefix . ':' . $elName;
157
158 22
		if (!$allowUnsafe && in_array($elName, $this->unsafeElements))
159
		{
160 1
			throw new RuntimeException("'" . $elName . "' elements are unsafe and are disabled by default. Please use " . __CLASS__ . '::allowUnsafeElement() to bypass this security measure');
161
		}
162
163
		// Retrieve or create the tag
164 21
		$tag = ($this->configurator->tags->exists($tagName))
165 1
		     ? $this->configurator->tags->get($tagName)
166 21
		     : $this->configurator->tags->add($tagName);
167
168
		// Rebuild this tag's template
169 21
		$this->rebuildTemplate($tag, $elName, $allowUnsafe);
170
171
		// Record the element name
172 21
		$this->elements[$elName] = 1;
173
174 21
		return $tag;
175
	}
176
177
	/**
178
	* Allow an attribute to be used in an HTML element
179
	*
180
	* @param  string $elName   Name of the element
181
	* @param  string $attrName Name of the attribute
182
	* @return \s9e\Configurator\Items\Attribute
0 ignored issues
show
Bug introduced by
The type s9e\Configurator\Items\Attribute was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
183
	*/
184 8
	public function allowAttribute($elName, $attrName)
185
	{
186 8
		return $this->allowAttributeWithSafety($elName, $attrName, false);
187
	}
188
189
	/**
190
	* Allow an unsafe attribute to be used in an HTML element
191
	*
192
	* @param  string $elName   Name of the element
193
	* @param  string $attrName Name of the attribute
194
	* @return \s9e\Configurator\Items\Attribute
195
	*/
196 3
	public function allowUnsafeAttribute($elName, $attrName)
197
	{
198 3
		return $this->allowAttributeWithSafety($elName, $attrName, true);
199
	}
200
201
	/**
202
	* Allow a (potentially unsafe) attribute to be used in an HTML element
203
	*
204
	* @param  string $elName   Name of the element
205
	* @param  string $attrName Name of the attribute
206
	* @param  bool   $allowUnsafe
207
	* @return \s9e\Configurator\Items\Attribute
208
	*/
209 11
	protected function allowAttributeWithSafety($elName, $attrName, $allowUnsafe)
210
	{
211 11
		$elName   = $this->normalizeElementName($elName);
212 11
		$attrName = $this->normalizeAttributeName($attrName);
213 10
		$tagName  = $this->prefix . ':' . $elName;
214
215 10
		if (!isset($this->elements[$elName]))
216
		{
217 1
			throw new RuntimeException("Element '" . $elName . "' has not been allowed");
218
		}
219
220 9
		if (!$allowUnsafe)
221
		{
222 6
			if (substr($attrName, 0, 2) === 'on'
223 6
			 || in_array($attrName, $this->unsafeAttributes))
224
			{
225 2
				throw new RuntimeException("'" . $attrName . "' attributes are unsafe and are disabled by default. Please use " . __CLASS__ . '::allowUnsafeAttribute() to bypass this security measure');
226
			}
227
		}
228
229 7
		$tag = $this->configurator->tags->get($tagName);
230 7
		if (!isset($tag->attributes[$attrName]))
231
		{
232 7
			$attribute = $tag->attributes->add($attrName);
233 7
			$attribute->required = false;
234
235 7
			if (isset($this->attributeFilters[$attrName]))
236
			{
237 1
				$filterName = $this->attributeFilters[$attrName];
238 1
				$filter = $this->configurator->attributeFilters->get($filterName);
239
240 1
				$attribute->filterChain->append($filter);
241
			}
242
		}
243
244
		// Rebuild this tag's template
245 7
		$this->rebuildTemplate($tag, $elName, $allowUnsafe);
246
247 7
		return $tag->attributes[$attrName];
248
	}
249
250
	/**
251
	* Validate and normalize an element name
252
	*
253
	* Accepts any name that would be valid, regardless of whether this element exists in HTML5.
254
	* Might be slightly off as the HTML5 specs don't seem to require it to start with a letter but
255
	* our implementation does.
256
	*
257
	* @link http://dev.w3.org/html5/spec/syntax.html#syntax-tag-name
258
	*
259
	* @param  string $elName Original element name
260
	* @return string         Normalized element name, in lowercase
261
	*/
262 28
	protected function normalizeElementName($elName)
263
	{
264 28
		if (!preg_match('#^[a-z][a-z0-9]*$#Di', $elName))
265
		{
266 1
			throw new InvalidArgumentException ("Invalid element name '" . $elName . "'");
267
		}
268
269 27
		return strtolower($elName);
270
	}
271
272
	/**
273
	* Validate and normalize an attribute name
274
	*
275
	* More restrictive than the specs but allows all HTML5 attributes and more.
276
	*
277
	* @param  string $attrName Original attribute name
278
	* @return string           Normalized attribute name, in lowercase
279
	*/
280 13
	protected function normalizeAttributeName($attrName)
281
	{
282 13
		if (!preg_match('#^[a-z][-\\w]*$#Di', $attrName))
283
		{
284 1
			throw new InvalidArgumentException ("Invalid attribute name '" . $attrName . "'");
285
		}
286
287 12
		return strtolower($attrName);
288
	}
289
290
	/**
291
	* Rebuild a tag's template
292
	*
293
	* @param  Tag    $tag         Source tag
294
	* @param  string $elName      Name of the HTML element created by the template
295
	* @param  bool   $allowUnsafe Whether to allow unsafe markup
296
	* @return void
297
	*/
298 21
	protected function rebuildTemplate(Tag $tag, $elName, $allowUnsafe)
299
	{
300 21
		$template = '<' . $elName . '>';
301 21
		foreach ($tag->attributes as $attrName => $attribute)
302
		{
303 7
			$template .= '<xsl:copy-of select="@' . $attrName . '"/>';
304
		}
305 21
		$template .= '<xsl:apply-templates/></' . $elName . '>';
306
307 21
		if ($allowUnsafe)
308
		{
309 5
			$template = new UnsafeTemplate($template);
310
		}
311
312 21
		$tag->setTemplate($template);
313
	}
314
315
	/**
316
	* Generate this plugin's config
317
	*
318
	* @return array|null
319
	*/
320 7
	public function asConfig()
321
	{
322 7
		if (empty($this->elements) && empty($this->aliases))
323
		{
324 1
			return;
325
		}
326
327
		/**
328
		* Regexp used to match an attributes definition (name + value if applicable)
329
		*
330
		* @link http://dev.w3.org/html5/spec/syntax.html#attributes-0
331
		*/
332 6
		$attrRegexp = '[a-z][-a-z0-9]*(?>\\s*=\\s*(?>"[^"]*"|\'[^\']*\'|[^\\s"\'=<>`]+))?';
333 6
		$tagRegexp  = RegexpBuilder::fromList(array_merge(
334 6
			array_keys($this->aliases),
335 6
			array_keys($this->elements)
336
		));
337
338 6
		$endTagRegexp   = '/(' . $tagRegexp . ')';
339 6
		$startTagRegexp = '(' . $tagRegexp . ')((?>\\s+' . $attrRegexp . ')*+)\\s*/?';
340
341 6
		$regexp = '#<(?>' . $endTagRegexp . '|' . $startTagRegexp . ')\\s*>#i';
342
343
		$config = [
344 6
			'quickMatch' => $this->quickMatch,
345 6
			'prefix'     => $this->prefix,
346 6
			'regexp'     => $regexp
347
		];
348
349 6
		if (!empty($this->aliases))
350
		{
351
			// Preserve the aliases array's keys in JavaScript
352 4
			$config['aliases'] = new Dictionary;
353 4
			foreach ($this->aliases as $elName => $aliases)
354
			{
355 4
				$config['aliases'][$elName] = new Dictionary($aliases);
356
			}
357
		}
358
359 6
		return $config;
360
	}
361
362
	/**
363
	* {@inheritdoc}
364
	*/
365 2
	public function getJSHints()
366
	{
367 2
		return ['HTMLELEMENTS_HAS_ALIASES' => (int) !empty($this->aliases)];
368
	}
369
}