Passed
Push — master ( 982d14...1bd195 )
by Josh
03:02
created

TemplateLoader::innerXML()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2020 The s9e authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter\Configurator\Helpers;
9
10
use DOMDocument;
11
use DOMElement;
12
use DOMXPath;
13
use RuntimeException;
14
15
abstract class TemplateLoader
16
{
17
	/**
18
	* XSL namespace
19
	*/
20
	const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';
21
22
	/**
23
	* Get the XML content of an element
24
	*
25
	* @private
26
	*
27
	* @param  DOMElement $element
28
	* @return string
29
	*/
30 15
	public static function innerXML(DOMElement $element)
31
	{
32
		// Serialize the XML then remove the outer element
33 15
		$xml = $element->ownerDocument->saveXML($element);
34 15
		$pos = 1 + strpos($xml, '>');
35 15
		$len = strrpos($xml, '<') - $pos;
36
37
		// If the template is empty, return an empty string
38 15
		return ($len < 1) ? '' : substr($xml, $pos, $len);
39
	}
40
41
	/**
42
	* Load a template as an xsl:template node
43
	*
44
	* Will attempt to load it as XML first, then as HTML as a fallback. Either way, an xsl:template
45
	* node is returned
46
	*
47
	* @param  string      $template
48
	* @return DOMDocument
49
	*/
50 24
	public static function load($template)
51
	{
52 24
		$dom = self::loadAsXML($template) ?: self::loadAsXML(self::fixEntities($template));
53 24
		if ($dom)
54
		{
55 13
			return $dom;
56
		}
57
58
		// If the template contains an XSL element, abort now. Otherwise, try reparsing it as HTML
59 11
		if (strpos($template, '<xsl:') !== false)
60
		{
61 1
			$error = libxml_get_last_error();
62
63 1
			throw new RuntimeException('Invalid XSL: ' . $error->message);
64
		}
65
66 10
		return self::loadAsHTML($template);
67
	}
68
69
	/**
70
	* Serialize a loaded template back into a string
71
	*
72
	* NOTE: removes the root node created by load()
73
	*
74
	* @param  DOMDocument $dom
75
	* @return string
76
	*/
77 6
	public static function save(DOMDocument $dom)
78
	{
79 6
		$xml = self::innerXML($dom->documentElement);
80 6
		if (strpos($xml, 'xmlns:xsl') !== false)
81
		{
82 1
			$xml = preg_replace('((<[^>]+?) xmlns:xsl="' . self::XMLNS_XSL . '")', '$1', $xml);
83
		}
84
85 6
		return $xml;
86
	}
87
88
	/**
89
	* Replace HTML entities and unescaped ampersands in given template
90
	*
91
	* @param  string $template
92
	* @return string
93
	*/
94 15
	protected static function fixEntities($template)
95
	{
96 15
		$template = self::replaceEntities($template);
97 15
		$template = preg_replace('(&(?!quot;|amp;|apos;|lt;|gt;|#\\d+;|#x[A-Fa-f0-9]+;))', '&amp;', $template);
98
99 15
		return $template;
100
	}
101
102
	/**
103
	* Load given HTML template in a DOM document
104
	*
105
	* @param  string      $template Original template
106
	* @return DOMDocument
107
	*/
108 10
	protected static function loadAsHTML($template)
109
	{
110 10
		$template = self::replaceCDATA($template);
111 10
		$template = self::replaceEntities($template);
112
113 10
		$dom  = new DOMDocument;
114 10
		$html = '<?xml version="1.0" encoding="utf-8" ?><html><body><div>' . $template . '</div></body></html>';
115
116 10
		$useErrors = libxml_use_internal_errors(true);
117 10
		$dom->loadHTML($html, LIBXML_NSCLEAN);
118 10
		self::removeInvalidAttributes($dom);
119 10
		libxml_use_internal_errors($useErrors);
120
121
		// Now dump the thing as XML then reload it with the proper root element
122 10
		$xml = '<?xml version="1.0" encoding="utf-8" ?><xsl:template xmlns:xsl="' . self::XMLNS_XSL . '">' . self::innerXML($dom->documentElement->firstChild->firstChild) . '</xsl:template>';
123
124 10
		$useErrors = libxml_use_internal_errors(true);
125 10
		$dom->loadXML($xml, LIBXML_NSCLEAN);
126 10
		libxml_use_internal_errors($useErrors);
127
128 10
		return $dom;
129
	}
130
131
	/**
132
	* Load given XSL template in a DOM document
133
	*
134
	* @param  string           $template Original template
135
	* @return bool|DOMDocument           DOMDocument on success, FALSE otherwise
136
	*/
137 24
	protected static function loadAsXML($template)
138
	{
139 24
		$xml = '<?xml version="1.0" encoding="utf-8" ?><xsl:template xmlns:xsl="' . self::XMLNS_XSL . '">' . $template . '</xsl:template>';
140
141 24
		$useErrors = libxml_use_internal_errors(true);
142 24
		$dom       = new DOMDocument;
143 24
		$success   = $dom->loadXML($xml, LIBXML_NOCDATA | LIBXML_NSCLEAN);
144 24
		self::removeInvalidAttributes($dom);
145 24
		libxml_use_internal_errors($useErrors);
146
147 24
		return ($success) ? $dom : false;
148
	}
149
150
	/**
151
	* Remove attributes with an invalid name from given DOM document
152
	*
153
	* @param  DOMDocument $dom
154
	* @return void
155
	*/
156 24
	protected static function removeInvalidAttributes(DOMDocument $dom)
157
	{
158 24
		$xpath = new DOMXPath($dom);
159 24
		foreach ($xpath->query('//@*') as $attribute)
160
		{
161 12
			if (!preg_match('(^(?:[-\\w]+:)?(?!\\d)[-\\w]+$)D', $attribute->nodeName))
162
			{
163 3
				$attribute->parentNode->removeAttributeNode($attribute);
164
			}
165
		}
166
	}
167
168
	/**
169
	* Replace CDATA sections in given template
170
	*
171
	* @param  string $template Original template
172
	* @return string           Modified template
173
	*/
174 10
	protected static function replaceCDATA($template)
175
	{
176 10
		return preg_replace_callback(
177 10
			'(<!\\[CDATA\\[(.*?)\\]\\]>)',
178
			function ($m)
179
			{
180 1
				return htmlspecialchars($m[1]);
181 10
			},
182
			$template
183
		);
184
	}
185
186
	/**
187
	* Replace known HTML entities
188
	*
189
	* @param  string $template
190
	* @return string
191
	*/
192 15
	protected static function replaceEntities(string $template): string
193
	{
194 15
		return preg_replace_callback(
195 15
			'(&(?!quot;|amp;|apos;|lt;|gt;)\\w+;)',
196
			function ($m)
197
			{
198 4
				return html_entity_decode($m[0], ENT_HTML5 | ENT_NOQUOTES, 'UTF-8');
199 15
			},
200
			$template
201
		);
202
	}
203
}