Completed
Push — master ( 2f74e5...c5dbe4 )
by Josh
17:21
created

TemplateLoader::fixEntities()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 1
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2018 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
	public static function innerXML(DOMElement $element)
31
	{
32
		// Serialize the XML then remove the outer element
33
		$xml = $element->ownerDocument->saveXML($element);
34
		$pos = 1 + strpos($xml, '>');
35
		$len = strrpos($xml, '<') - $pos;
36
37
		// If the template is empty, return an empty string
38
		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
	public static function load($template)
51
	{
52
		$dom = self::loadAsXML($template) ?: self::loadAsXML(self::fixEntities($template));
53
		if ($dom)
54
		{
55
			return $dom;
56
		}
57
58
		// If the template contains an XSL element, abort now. Otherwise, try reparsing it as HTML
59
		if (strpos($template, '<xsl:') !== false)
60
		{
61
			$error = libxml_get_last_error();
62
63
			throw new RuntimeException('Invalid XSL: ' . $error->message);
64
		}
65
66
		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
	public static function save(DOMDocument $dom)
78
	{
79
		return self::innerXML($dom->documentElement);
80
	}
81
82
	/**
83
	* Replace HTML entities and unescaped ampersands in given template
84
	*
85
	* @param  string $template
86
	* @return string
87
	*/
88
	protected static function fixEntities($template)
89
	{
90
		return preg_replace_callback(
91
			'(&(?!quot;|amp;|apos;|lt;|gt;)\\w+;)',
92
			function ($m)
93
			{
94
				return html_entity_decode($m[0], ENT_NOQUOTES, 'UTF-8');
95
			},
96
			preg_replace('(&(?![A-Za-z0-9]+;|#\\d+;|#x[A-Fa-f0-9]+;))', '&amp;', $template)
97
		);
98
	}
99
100
	/**
101
	* Load given HTML template in a DOM document
102
	*
103
	* @param  string      $template Original template
104
	* @return DOMDocument
105
	*/
106
	protected static function loadAsHTML($template)
107
	{
108
		$dom  = new DOMDocument;
109
		$html = '<?xml version="1.0" encoding="utf-8" ?><html><body><div>' . $template . '</div></body></html>';
110
111
		$useErrors = libxml_use_internal_errors(true);
112
		$dom->loadHTML($html);
113
		self::removeInvalidAttributes($dom);
114
		libxml_use_internal_errors($useErrors);
115
116
		// Now dump the thing as XML then reload it with the proper root element
117
		$xml = '<?xml version="1.0" encoding="utf-8" ?><xsl:template xmlns:xsl="' . self::XMLNS_XSL . '">' . self::innerXML($dom->documentElement->firstChild->firstChild) . '</xsl:template>';
118
119
		$useErrors = libxml_use_internal_errors(true);
120
		$dom->loadXML($xml);
121
		libxml_use_internal_errors($useErrors);
122
123
		return $dom;
124
	}
125
126
	/**
127
	* Load given XSL template in a DOM document
128
	*
129
	* @param  string           $template Original template
130
	* @return bool|DOMDocument           DOMDocument on success, FALSE otherwise
131
	*/
132
	protected static function loadAsXML($template)
133
	{
134
		$xml = '<?xml version="1.0" encoding="utf-8" ?><xsl:template xmlns:xsl="' . self::XMLNS_XSL . '">' . $template . '</xsl:template>';
135
136
		$useErrors = libxml_use_internal_errors(true);
137
		$dom       = new DOMDocument;
138
		$success   = $dom->loadXML($xml);
139
		self::removeInvalidAttributes($dom);
140
		libxml_use_internal_errors($useErrors);
141
142
		return ($success) ? $dom : false;
143
	}
144
145
	/**
146
	* Remove attributes with an invalid name from given DOM document
147
	*
148
	* @param  DOMDocument $dom
149
	* @return void
150
	*/
151
	protected static function removeInvalidAttributes(DOMDocument $dom)
152
	{
153
		$xpath = new DOMXPath($dom);
154
		foreach ($xpath->query('//@*') as $attribute)
155
		{
156
			if (!preg_match('(^(?:[-\\w]+:)?(?!\\d)[-\\w]+$)D', $attribute->nodeName))
157
			{
158
				$attribute->parentNode->removeAttributeNode($attribute);
159
			}
160
		}
161
	}
162
}