PluginProbe ʕ •ᴥ•ʔ
Paid Membership Plugin, Ecommerce, User Registration Form, Login Form, User Profile & Restrict Content – ProfilePress / 4.17.1
Paid Membership Plugin, Ecommerce, User Registration Form, Login Form, User Profile & Restrict Content – ProfilePress v4.17.1
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 2 months ago CssToAttributeConverter.php 2 months ago HtmlNormalizer.php 2 months ago HtmlPruner.php 2 months ago
AbstractHtmlProcessor.php
402 lines
1 <?php
2
3 declare (strict_types=1);
4 namespace ProfilePressVendor\Pelago\Emogrifier\HtmlProcessor;
5
6 /**
7 * Base class for HTML processor that e.g., can remove, add or modify nodes or attributes.
8 *
9 * The "vanilla" subclass is the HtmlNormalizer.
10 *
11 * @psalm-consistent-constructor
12 */
13 abstract class AbstractHtmlProcessor
14 {
15 /**
16 * @var string
17 */
18 protected const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>';
19 /**
20 * @var string
21 */
22 protected const CONTENT_TYPE_META_TAG = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
23 /**
24 * @var string Regular expression part to match tag names that PHP's DOMDocument implementation is not aware are
25 * self-closing. These are mostly HTML5 elements, but for completeness <command> (obsolete) and <keygen>
26 * (deprecated) are also included.
27 *
28 * @see https://bugs.php.net/bug.php?id=73175
29 */
30 protected const PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER = '(?:command|embed|keygen|source|track|wbr)';
31 /**
32 * Regular expression part to match tag names that may appear before the start of the `<body>` element. A start tag
33 * for any other element would implicitly start the `<body>` element due to tag omission rules.
34 *
35 * @var string
36 */
37 protected const TAGNAME_ALLOWED_BEFORE_BODY_MATCHER = '(?:html|head|base|command|link|meta|noscript|script|style|template|title)';
38 /**
39 * regular expression pattern to match an HTML comment, including delimiters and modifiers
40 *
41 * @var string
42 */
43 protected const HTML_COMMENT_PATTERN = '/<!--[^-]*+(?:-(?!->)[^-]*+)*+(?:-->|$)/';
44 /**
45 * regular expression pattern to match an HTML `<template>` element, including delimiters and modifiers
46 *
47 * @var string
48 */
49 protected const HTML_TEMPLATE_ELEMENT_PATTERN = '%<template[\s>][^<]*+(?:<(?!/template>)[^<]*+)*+(?:</template>|$)%i';
50 /**
51 * @var ?\DOMDocument
52 */
53 protected $domDocument = null;
54 /**
55 * @var ?\DOMXPath
56 */
57 private $xPath = null;
58 /**
59 * The constructor.
60 *
61 * Please use `::fromHtml` or `::fromDomDocument` instead.
62 */
63 private function __construct()
64 {
65 }
66 /**
67 * Builds a new instance from the given HTML.
68 *
69 * @param string $unprocessedHtml raw HTML, must be UTF-encoded, must not be empty
70 *
71 * @return static
72 *
73 * @throws \InvalidArgumentException if $unprocessedHtml is anything other than a non-empty string
74 */
75 public static function fromHtml(string $unprocessedHtml): self
76 {
77 if ($unprocessedHtml === '') {
78 throw new \InvalidArgumentException('The provided HTML must not be empty.', 1515763647);
79 }
80 $instance = new static();
81 $instance->setHtml($unprocessedHtml);
82 return $instance;
83 }
84 /**
85 * Builds a new instance from the given DOM document.
86 *
87 * @param \DOMDocument $document a DOM document returned by getDomDocument() of another instance
88 *
89 * @return static
90 */
91 public static function fromDomDocument(\DOMDocument $document): self
92 {
93 $instance = new static();
94 $instance->setDomDocument($document);
95 return $instance;
96 }
97 /**
98 * Sets the HTML to process.
99 *
100 * @param string $html the HTML to process, must be UTF-8-encoded
101 */
102 private function setHtml(string $html): void
103 {
104 $this->createUnifiedDomDocument($html);
105 }
106 /**
107 * Provides access to the internal DOMDocument representation of the HTML in its current state.
108 *
109 * @return \DOMDocument
110 *
111 * @throws \UnexpectedValueException
112 */
113 public function getDomDocument(): \DOMDocument
114 {
115 if (!$this->domDocument instanceof \DOMDocument) {
116 $message = self::class . '::setDomDocument() has not yet been called on ' . static::class;
117 throw new \UnexpectedValueException($message, 1570472239);
118 }
119 return $this->domDocument;
120 }
121 /**
122 * @param \DOMDocument $domDocument
123 */
124 private function setDomDocument(\DOMDocument $domDocument): void
125 {
126 $this->domDocument = $domDocument;
127 $this->xPath = new \DOMXPath($this->domDocument);
128 }
129 /**
130 * @return \DOMXPath
131 *
132 * @throws \UnexpectedValueException
133 */
134 protected function getXPath(): \DOMXPath
135 {
136 if (!$this->xPath instanceof \DOMXPath) {
137 $message = self::class . '::setDomDocument() has not yet been called on ' . static::class;
138 throw new \UnexpectedValueException($message, 1617819086);
139 }
140 return $this->xPath;
141 }
142 /**
143 * Renders the normalized and processed HTML.
144 *
145 * @return string
146 */
147 public function render(): string
148 {
149 $htmlWithPossibleErroneousClosingTags = $this->getDomDocument()->saveHTML();
150 return $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags);
151 }
152 /**
153 * Renders the content of the BODY element of the normalized and processed HTML.
154 *
155 * @return string
156 */
157 public function renderBodyContent(): string
158 {
159 $htmlWithPossibleErroneousClosingTags = $this->getDomDocument()->saveHTML($this->getBodyElement());
160 $bodyNodeHtml = $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags);
161 return \preg_replace('%</?+body(?:\s[^>]*+)?+>%', '', $bodyNodeHtml);
162 }
163 /**
164 * Eliminates any invalid closing tags for void elements from the given HTML.
165 *
166 * @param string $html
167 *
168 * @return string
169 */
170 private function removeSelfClosingTagsClosingTags(string $html): string
171 {
172 return \preg_replace('%</' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '>%', '', $html);
173 }
174 /**
175 * Returns the BODY element.
176 *
177 * This method assumes that there always is a BODY element.
178 *
179 * @return \DOMElement
180 *
181 * @throws \RuntimeException
182 */
183 private function getBodyElement(): \DOMElement
184 {
185 $node = $this->getDomDocument()->getElementsByTagName('body')->item(0);
186 if (!$node instanceof \DOMElement) {
187 throw new \RuntimeException('There is no body element.', 1617922607);
188 }
189 return $node;
190 }
191 /**
192 * Creates a DOM document from the given HTML and stores it in $this->domDocument.
193 *
194 * The DOM document will always have a BODY element and a document type.
195 *
196 * @param string $html
197 */
198 private function createUnifiedDomDocument(string $html): void
199 {
200 $this->createRawDomDocument($html);
201 $this->ensureExistenceOfBodyElement();
202 }
203 /**
204 * Creates a DOMDocument instance from the given HTML and stores it in $this->domDocument.
205 *
206 * @param string $html
207 */
208 private function createRawDomDocument(string $html): void
209 {
210 $domDocument = new \DOMDocument();
211 $domDocument->strictErrorChecking = \false;
212 $domDocument->formatOutput = \true;
213 $libXmlState = \libxml_use_internal_errors(\true);
214 $domDocument->loadHTML($this->prepareHtmlForDomConversion($html));
215 \libxml_clear_errors();
216 \libxml_use_internal_errors($libXmlState);
217 $this->setDomDocument($domDocument);
218 }
219 /**
220 * Returns the HTML with added document type, Content-Type meta tag, and self-closing slashes, if needed,
221 * ensuring that the HTML will be good for creating a DOM document from it.
222 *
223 * @param string $html
224 *
225 * @return string the unified HTML
226 */
227 private function prepareHtmlForDomConversion(string $html): string
228 {
229 $htmlWithSelfClosingSlashes = $this->ensurePhpUnrecognizedSelfClosingTagsAreXml($html);
230 $htmlWithDocumentType = $this->ensureDocumentType($htmlWithSelfClosingSlashes);
231 return $this->addContentTypeMetaTag($htmlWithDocumentType);
232 }
233 /**
234 * Makes sure that the passed HTML has a document type, with lowercase "html".
235 *
236 * @param string $html
237 *
238 * @return string HTML with document type
239 */
240 private function ensureDocumentType(string $html): string
241 {
242 $hasDocumentType = \stripos($html, '<!DOCTYPE') !== \false;
243 if ($hasDocumentType) {
244 return $this->normalizeDocumentType($html);
245 }
246 return self::DEFAULT_DOCUMENT_TYPE . $html;
247 }
248 /**
249 * Makes sure the document type in the passed HTML has lowercase "html".
250 *
251 * @param string $html
252 *
253 * @return string HTML with normalized document type
254 */
255 private function normalizeDocumentType(string $html): string
256 {
257 // Limit to replacing the first occurrence: as an optimization; and in case an example exists as unescaped text.
258 return \preg_replace('/<!DOCTYPE\s++html(?=[\s>])/i', '<!DOCTYPE html', $html, 1);
259 }
260 /**
261 * Adds a Content-Type meta tag for the charset.
262 *
263 * This method also ensures that there is a HEAD element.
264 *
265 * @param string $html
266 *
267 * @return string the HTML with the meta tag added
268 */
269 private function addContentTypeMetaTag(string $html): string
270 {
271 if ($this->hasContentTypeMetaTagInHead($html)) {
272 return $html;
273 }
274 // We are trying to insert the meta tag to the right spot in the DOM.
275 // If we just prepended it to the HTML, we would lose attributes set to the HTML tag.
276 $hasHeadTag = \preg_match('/<head[\s>]/i', $html);
277 $hasHtmlTag = \stripos($html, '<html') !== \false;
278 if ($hasHeadTag) {
279 $reworkedHtml = \preg_replace('/<head(?=[\s>])([^>]*+)>/i', '<head$1>' . self::CONTENT_TYPE_META_TAG, $html);
280 } elseif ($hasHtmlTag) {
281 $reworkedHtml = \preg_replace('/<html(.*?)>/is', '<html$1><head>' . self::CONTENT_TYPE_META_TAG . '</head>', $html);
282 } else {
283 $reworkedHtml = self::CONTENT_TYPE_META_TAG . $html;
284 }
285 return $reworkedHtml;
286 }
287 /**
288 * Tests whether the given HTML has a valid `Content-Type` metadata element within the `<head>` element. Due to tag
289 * omission rules, HTML parsers are expected to end the `<head>` element and start the `<body>` element upon
290 * encountering a start tag for any element which is permitted only within the `<body>`.
291 *
292 * @param string $html
293 *
294 * @return bool
295 */
296 private function hasContentTypeMetaTagInHead(string $html): bool
297 {
298 \preg_match('%^.*?(?=<meta(?=\s)[^>]*\shttp-equiv=(["\']?+)Content-Type\g{-1}[\s/>])%is', $html, $matches);
299 if (isset($matches[0])) {
300 $htmlBefore = $matches[0];
301 try {
302 $hasContentTypeMetaTagInHead = !$this->hasEndOfHeadElement($htmlBefore);
303 } catch (\RuntimeException $exception) {
304 // If something unexpected occurs, assume the `Content-Type` that was found is valid.
305 \trigger_error($exception->getMessage());
306 $hasContentTypeMetaTagInHead = \true;
307 }
308 } else {
309 $hasContentTypeMetaTagInHead = \false;
310 }
311 return $hasContentTypeMetaTagInHead;
312 }
313 /**
314 * Tests whether the `<head>` element ends within the given HTML. Due to tag omission rules, HTML parsers are
315 * expected to end the `<head>` element and start the `<body>` element upon encountering a start tag for any element
316 * which is permitted only within the `<body>`.
317 *
318 * @param string $html
319 *
320 * @return bool
321 *
322 * @throws \RuntimeException
323 */
324 private function hasEndOfHeadElement(string $html): bool
325 {
326 $headEndTagMatchCount = \preg_match('%<(?!' . self::TAGNAME_ALLOWED_BEFORE_BODY_MATCHER . '[\s/>])\w|</head>%i', $html);
327 if (\is_int($headEndTagMatchCount) && $headEndTagMatchCount > 0) {
328 // An exception to the implicit end of the `<head>` is any content within a `<template>` element, as well in
329 // comments. As an optimization, this is only checked for if a potential `<head>` end tag is found.
330 $htmlWithoutCommentsOrTemplates = $this->removeHtmlTemplateElements($this->removeHtmlComments($html));
331 $hasEndOfHeadElement = $htmlWithoutCommentsOrTemplates === $html || $this->hasEndOfHeadElement($htmlWithoutCommentsOrTemplates);
332 } else {
333 $hasEndOfHeadElement = \false;
334 }
335 return $hasEndOfHeadElement;
336 }
337 /**
338 * Removes comments from the given HTML, including any which are unterminated, for which the remainder of the string
339 * is removed.
340 *
341 * @param string $html
342 *
343 * @return string
344 *
345 * @throws \RuntimeException
346 */
347 private function removeHtmlComments(string $html): string
348 {
349 $result = \preg_replace(self::HTML_COMMENT_PATTERN, '', $html);
350 if (!\is_string($result)) {
351 throw new \RuntimeException('Internal PCRE error', 1616521475);
352 }
353 return $result;
354 }
355 /**
356 * Removes `<template>` elements from the given HTML, including any without an end tag, for which the remainder of
357 * the string is removed.
358 *
359 * @param string $html
360 *
361 * @return string
362 *
363 * @throws \RuntimeException
364 */
365 private function removeHtmlTemplateElements(string $html): string
366 {
367 $result = \preg_replace(self::HTML_TEMPLATE_ELEMENT_PATTERN, '', $html);
368 if (!\is_string($result)) {
369 throw new \RuntimeException('Internal PCRE error', 1616519652);
370 }
371 return $result;
372 }
373 /**
374 * Makes sure that any self-closing tags not recognized as such by PHP's DOMDocument implementation have a
375 * self-closing slash.
376 *
377 * @param string $html
378 *
379 * @return string HTML with problematic tags converted.
380 */
381 private function ensurePhpUnrecognizedSelfClosingTagsAreXml(string $html): string
382 {
383 return \preg_replace('%<' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '\b[^>]*+(?<!/)(?=>)%', '$0/', $html);
384 }
385 /**
386 * Checks that $this->domDocument has a BODY element and adds it if it is missing.
387 *
388 * @throws \UnexpectedValueException
389 */
390 private function ensureExistenceOfBodyElement(): void
391 {
392 if ($this->getDomDocument()->getElementsByTagName('body')->item(0) instanceof \DOMElement) {
393 return;
394 }
395 $htmlElement = $this->getDomDocument()->getElementsByTagName('html')->item(0);
396 if (!$htmlElement instanceof \DOMElement) {
397 throw new \UnexpectedValueException('There is no HTML element although there should be one.', 1569930853);
398 }
399 $htmlElement->appendChild($this->getDomDocument()->createElement('body'));
400 }
401 }
402