PluginProbe ʕ •ᴥ•ʔ
Paid Membership Plugin, Ecommerce, User Registration Form, Login Form, User Profile & Restrict Content – ProfilePress / trunk
Paid Membership Plugin, Ecommerce, User Registration Form, Login Form, User Profile & Restrict Content – ProfilePress vtrunk
4.17.2 4.17.1 4.17.0 4.16.19 4.16.18 4.16.17 4.16.16 trunk 1.0 1.0.1 1.0.2 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5a 1.1.6 1.1.7 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.4 1.4.1 1.4.2 1.5 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.7 1.7.1 1.7.2 1.8 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.1.9 2.2.10 2.2.11 2.2.12 2.2.13 2.2.14 2.2.15 2.2.16 2.2.2 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 3.0 3.1 3.1.1 3.1.10 3.1.11 3.1.12 3.1.13 3.1.14 3.1.15 3.1.16 3.1.17 3.1.18 3.1.19 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.1 3.2.10 3.2.11 3.2.12 3.2.13 3.2.14 3.2.15 3.2.16 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 3.2.9 4.0.0 4.0.1 4.0.2 4.0.3 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.10.0 4.10.1 4.10.2 4.10.3 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.13.3 4.13.4 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.15.0 4.15.1 4.15.10 4.15.11 4.15.12 4.15.13 4.15.14 4.15.15 4.15.16 4.15.17 4.15.18 4.15.19 4.15.2 4.15.20 4.15.20.1 4.15.21 4.15.22 4.15.23 4.15.24 4.15.25 4.15.3 4.15.4 4.15.5 4.15.6 4.15.7 4.15.8 4.15.9 4.16.0 4.16.1 4.16.10 4.16.11 4.16.12 4.16.13 4.16.14 4.16.15 4.16.2 4.16.3 4.16.4 4.16.5 4.16.6 4.16.7 4.16.8 4.16.9 4.2.0 4.3.0 4.3.1 4.3.2 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.7.0 4.8.0 4.9.0
wp-user-avatar / third-party / vendor / pelago / emogrifier / src / HtmlProcessor / AbstractHtmlProcessor.php
wp-user-avatar / third-party / vendor / pelago / emogrifier / src / HtmlProcessor Last commit date
AbstractHtmlProcessor.php 6 days ago CssToAttributeConverter.php 6 days ago CssVariableEvaluator.php 6 days ago HtmlNormalizer.php 6 days ago HtmlPruner.php 6 days ago
AbstractHtmlProcessor.php
383 lines
1 <?php
2
3 declare (strict_types=1);
4 namespace ProfilePressVendor\Pelago\Emogrifier\HtmlProcessor;
5
6 use function ProfilePressVendor\Safe\preg_match;
7 use function ProfilePressVendor\Safe\preg_replace;
8 /**
9 * Base class for HTML processor that e.g., can remove, add or modify nodes or attributes.
10 *
11 * The "vanilla" subclass is the HtmlNormalizer.
12 */
13 abstract class AbstractHtmlProcessor
14 {
15 protected const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>';
16 protected const CONTENT_TYPE_META_TAG = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
17 /**
18 * Regular expression part to match tag names that PHP's DOMDocument implementation is not
19 * aware are self-closing. These are mostly HTML5 elements, but for completeness `<command>` (obsolete) and
20 * `<keygen>` (deprecated) are also included.
21 *
22 * @see https://bugs.php.net/bug.php?id=73175
23 */
24 protected const PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER = '(?:command|embed|keygen|source|track|wbr)';
25 /**
26 * Regular expression part to match tag names that may appear before the start of the `<body>` element. A start tag
27 * for any other element would implicitly start the `<body>` element due to tag omission rules.
28 */
29 protected const TAGNAME_ALLOWED_BEFORE_BODY_MATCHER = '(?:html|head|base|command|link|meta|noscript|script|style|template|title)';
30 /**
31 * regular expression pattern to match an HTML comment, including delimiters and modifiers
32 */
33 protected const HTML_COMMENT_PATTERN = '/<!--[^-]*+(?:-(?!->)[^-]*+)*+(?:-->|$)/';
34 /**
35 * regular expression pattern to match an HTML `<template>` element, including delimiters and modifiers
36 */
37 protected const HTML_TEMPLATE_ELEMENT_PATTERN = '%<template[\s>][^<]*+(?:<(?!/template>)[^<]*+)*+(?:</template>|$)%i';
38 /**
39 * @var \DOMDocument|null
40 */
41 protected $domDocument = null;
42 /**
43 * @var \DOMXPath|null
44 */
45 private $xPath = null;
46 /**
47 * The constructor.
48 *
49 * Please use `::fromHtml` or `::fromDomDocument` instead.
50 */
51 private function __construct()
52 {
53 }
54 /**
55 * Builds a new instance from the given HTML.
56 *
57 * @param non-empty-string $unprocessedHtml raw HTML, must be UTF-encoded
58 *
59 * @return static
60 *
61 * @throws \InvalidArgumentException if $unprocessedHtml is anything other than a non-empty string
62 */
63 public static function fromHtml(string $unprocessedHtml): self
64 {
65 // @phpstan-ignore-next-line argument.type We're checking for a contract violation here.
66 if ($unprocessedHtml === '') {
67 throw new \InvalidArgumentException('The provided HTML must not be empty.', 1515763647);
68 }
69 $instance = new static();
70 $instance->setHtml($unprocessedHtml);
71 return $instance;
72 }
73 /**
74 * Builds a new instance from the given DOM document.
75 *
76 * @param \DOMDocument $document a DOM document returned by getDomDocument() of another instance
77 *
78 * @return static
79 */
80 public static function fromDomDocument(\DOMDocument $document): self
81 {
82 $instance = new static();
83 $instance->setDomDocument($document);
84 return $instance;
85 }
86 /**
87 * Sets the HTML to process.
88 *
89 * @param string $html the HTML to process, must be UTF-8-encoded
90 */
91 private function setHtml(string $html): void
92 {
93 $this->createUnifiedDomDocument($html);
94 }
95 /**
96 * Provides access to the internal DOMDocument representation of the HTML in its current state.
97 *
98 * @throws \UnexpectedValueException
99 */
100 public function getDomDocument(): \DOMDocument
101 {
102 if (!$this->domDocument instanceof \DOMDocument) {
103 $message = self::class . '::setDomDocument() has not yet been called on ' . static::class;
104 throw new \UnexpectedValueException($message, 1570472239);
105 }
106 return $this->domDocument;
107 }
108 private function setDomDocument(\DOMDocument $domDocument): void
109 {
110 $this->domDocument = $domDocument;
111 $this->xPath = new \DOMXPath($this->domDocument);
112 }
113 /**
114 * @throws \UnexpectedValueException
115 */
116 protected function getXPath(): \DOMXPath
117 {
118 if (!$this->xPath instanceof \DOMXPath) {
119 $message = self::class . '::setDomDocument() has not yet been called on ' . static::class;
120 throw new \UnexpectedValueException($message, 1617819086);
121 }
122 return $this->xPath;
123 }
124 /**
125 * Renders the normalized and processed HTML.
126 */
127 public function render(): string
128 {
129 $htmlWithPossibleErroneousClosingTags = $this->getDomDocument()->saveHTML();
130 return $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags);
131 }
132 /**
133 * Renders the content of the BODY element of the normalized and processed HTML.
134 */
135 public function renderBodyContent(): string
136 {
137 $htmlWithPossibleErroneousClosingTags = $this->getDomDocument()->saveHTML($this->getBodyElement());
138 $bodyNodeHtml = $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags);
139 return preg_replace('%</?+body(?:\s[^>]*+)?+>%', '', $bodyNodeHtml);
140 }
141 /**
142 * Eliminates any invalid closing tags for void elements from the given HTML.
143 */
144 private function removeSelfClosingTagsClosingTags(string $html): string
145 {
146 return preg_replace('%</' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '>%', '', $html);
147 }
148 /**
149 * Returns the HTML element.
150 *
151 * This method assumes that there always is an HTML element, throwing an exception otherwise.
152 *
153 * @throws \UnexpectedValueException
154 */
155 protected function getHtmlElement(): \DOMElement
156 {
157 $htmlElement = $this->getDomDocument()->getElementsByTagName('html')->item(0);
158 if (!$htmlElement instanceof \DOMElement) {
159 throw new \UnexpectedValueException('There is no HTML element although there should be one.', 1569930853);
160 }
161 return $htmlElement;
162 }
163 /**
164 * Returns the BODY element.
165 *
166 * This method assumes that there always is a BODY element.
167 *
168 * @throws \RuntimeException
169 */
170 private function getBodyElement(): \DOMElement
171 {
172 $node = $this->getDomDocument()->getElementsByTagName('body')->item(0);
173 if (!$node instanceof \DOMElement) {
174 throw new \RuntimeException('There is no body element.', 1617922607);
175 }
176 return $node;
177 }
178 /**
179 * Creates a DOM document from the given HTML and stores it in $this->domDocument.
180 *
181 * The DOM document will always have a BODY element and a document type.
182 */
183 private function createUnifiedDomDocument(string $html): void
184 {
185 $this->createRawDomDocument($html);
186 $this->ensureExistenceOfBodyElement();
187 }
188 /**
189 * Creates a DOMDocument instance from the given HTML and stores it in $this->domDocument.
190 */
191 private function createRawDomDocument(string $html): void
192 {
193 $domDocument = new \DOMDocument();
194 $domDocument->strictErrorChecking = \false;
195 $domDocument->formatOutput = \false;
196 $libXmlState = \libxml_use_internal_errors(\true);
197 $domDocument->loadHTML($this->prepareHtmlForDomConversion($html));
198 \libxml_clear_errors();
199 \libxml_use_internal_errors($libXmlState);
200 $this->setDomDocument($domDocument);
201 }
202 /**
203 * Returns the HTML with added document type, Content-Type meta tag, and self-closing slashes, if needed,
204 * ensuring that the HTML will be good for creating a DOM document from it.
205 */
206 private function prepareHtmlForDomConversion(string $html): string
207 {
208 $htmlWithSelfClosingSlashes = $this->ensurePhpUnrecognizedSelfClosingTagsAreXml($html);
209 $htmlWithDocumentType = $this->ensureDocumentType($htmlWithSelfClosingSlashes);
210 return $this->addContentTypeMetaTag($htmlWithDocumentType);
211 }
212 /**
213 * Makes sure that the passed HTML has a document type, with lowercase "html".
214 *
215 * @return non-empty-string HTML with document type
216 */
217 private function ensureDocumentType(string $html): string
218 {
219 $hasDocumentType = \stripos($html, '<!DOCTYPE') !== \false;
220 if ($hasDocumentType) {
221 return $this->normalizeDocumentType($html);
222 }
223 return self::DEFAULT_DOCUMENT_TYPE . $html;
224 }
225 /**
226 * Makes sure the document type in the passed HTML has lowercase `html`.
227 *
228 * @param non-empty-string $html
229 *
230 * @return non-empty-string HTML with normalized document type
231 */
232 private function normalizeDocumentType(string $html): string
233 {
234 // Limit to replacing the first occurrence: as an optimization; and in case an example exists as unescaped text.
235 $result = preg_replace('/<!DOCTYPE\s++html(?=[\s>])/i', '<!DOCTYPE html', $html, 1);
236 \assert($result !== '');
237 return $result;
238 }
239 /**
240 * Adds a Content-Type meta tag for the charset.
241 *
242 * This method also ensures that there is a HEAD element.
243 *
244 * @param non-empty-string $html
245 *
246 * @return non-empty-string
247 */
248 private function addContentTypeMetaTag(string $html): string
249 {
250 if ($this->hasContentTypeMetaTagInHead($html)) {
251 return $html;
252 }
253 // We are trying to insert the meta tag to the right spot in the DOM.
254 // If we just prepended it to the HTML, we would lose attributes set to the HTML tag.
255 $hasHeadTag = preg_match('/<head[\s>]/i', $html) !== 0;
256 $hasHtmlTag = \stripos($html, '<html') !== \false;
257 if ($hasHeadTag) {
258 $reworkedHtml = preg_replace('/<head(?=[\s>])([^>]*+)>/i', '<head$1>' . self::CONTENT_TYPE_META_TAG, $html);
259 } elseif ($hasHtmlTag) {
260 $reworkedHtml = preg_replace('/<html(.*?)>/is', '<html$1><head>' . self::CONTENT_TYPE_META_TAG . '</head>', $html);
261 } else {
262 $reworkedHtml = self::CONTENT_TYPE_META_TAG . $html;
263 }
264 \assert($reworkedHtml !== '');
265 return $reworkedHtml;
266 }
267 /**
268 * Tests whether the given HTML has a valid `Content-Type` metadata element within the `<head>` element. Due to tag
269 * omission rules, HTML parsers are expected to end the `<head>` element and start the `<body>` element upon
270 * encountering a start tag for any element which is permitted only within the `<body>`.
271 */
272 private function hasContentTypeMetaTagInHead(string $html): bool
273 {
274 preg_match('%
275 (?(DEFINE)
276 # the target `http-equiv` attribute match
277 (?<target_attribute>
278 http-equiv=(["\']?+)Content-Type\g{-1}
279 # must be followed by one of these characters
280 [\s/>]
281 )
282 # the target `meta` element match without the opening `<`
283 (?<target>
284 meta(?=\s)
285 # one or other of these
286 (?:
287 # one or more characters other than `>` or space
288 [^>\s]++
289 |
290 # space not followed by the target `http-equiv` attribute
291 \s(?!(?&target_attribute))
292 )
293 # any number of times (including zero)
294 *+
295 \s(?&target_attribute)
296 )
297 )
298 # start of `subject`
299 ^
300 # one or other of these
301 (?:
302 # one or more characters other than `<`
303 [^<]++
304 |
305 # `<` not followed by `target`
306 <(?!(?&target))
307 )
308 # any number of times (including zero)
309 *+
310 # followed by the target, not captured
311 (?=<(?&target))
312 %isx', $html, $matches);
313 if (isset($matches[0])) {
314 $htmlBefore = $matches[0];
315 try {
316 $hasContentTypeMetaTagInHead = !$this->hasEndOfHeadElement($htmlBefore);
317 } catch (\RuntimeException $exception) {
318 // If something unexpected occurs, assume the `Content-Type` that was found is valid.
319 \trigger_error($exception->getMessage());
320 $hasContentTypeMetaTagInHead = \true;
321 }
322 } else {
323 $hasContentTypeMetaTagInHead = \false;
324 }
325 return $hasContentTypeMetaTagInHead;
326 }
327 /**
328 * Tests whether the `<head>` element ends within the given HTML. Due to tag omission rules, HTML parsers are
329 * expected to end the `<head>` element and start the `<body>` element upon encountering a start tag for any element
330 * which is permitted only within the `<body>`.
331 *
332 * @throws \RuntimeException
333 */
334 private function hasEndOfHeadElement(string $html): bool
335 {
336 if (preg_match('%<(?!' . self::TAGNAME_ALLOWED_BEFORE_BODY_MATCHER . '[\s/>])\w|</head>%i', $html) !== 0) {
337 // An exception to the implicit end of the `<head>` is any content within a `<template>` element, as well in
338 // comments. As an optimization, this is only checked for if a potential `<head>` end tag is found.
339 $htmlWithoutCommentsOrTemplates = $this->removeHtmlTemplateElements($this->removeHtmlComments($html));
340 $hasEndOfHeadElement = $htmlWithoutCommentsOrTemplates === $html || $this->hasEndOfHeadElement($htmlWithoutCommentsOrTemplates);
341 } else {
342 $hasEndOfHeadElement = \false;
343 }
344 return $hasEndOfHeadElement;
345 }
346 /**
347 * Removes comments from the given HTML, including any which are unterminated, for which the remainder of the string
348 * is removed.
349 */
350 private function removeHtmlComments(string $html): string
351 {
352 return preg_replace(self::HTML_COMMENT_PATTERN, '', $html);
353 }
354 /**
355 * Removes `<template>` elements from the given HTML, including any without an end tag, for which the remainder of
356 * the string is removed.
357 */
358 private function removeHtmlTemplateElements(string $html): string
359 {
360 return preg_replace(self::HTML_TEMPLATE_ELEMENT_PATTERN, '', $html);
361 }
362 /**
363 * Makes sure that any self-closing tags not recognized as such by PHP's DOMDocument implementation have a
364 * self-closing slash.
365 */
366 private function ensurePhpUnrecognizedSelfClosingTagsAreXml(string $html): string
367 {
368 return preg_replace('%<' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '\b[^>]*+(?<!/)(?=>)%', '$0/', $html);
369 }
370 /**
371 * Checks that $this->domDocument has a BODY element and adds it if it is missing.
372 *
373 * @throws \UnexpectedValueException
374 */
375 private function ensureExistenceOfBodyElement(): void
376 {
377 if ($this->getDomDocument()->getElementsByTagName('body')->item(0) instanceof \DOMElement) {
378 return;
379 }
380 $this->getHtmlElement()->appendChild($this->getDomDocument()->createElement('body'));
381 }
382 }
383