PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / vendor_prefixed / masterminds / html5 / src / HTML5 / Parser / DOMTreeBuilder.php

DOMTreeBuilder.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.18, at vendor_prefixed/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php

571 lines 22.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WCPOS\Vendor\Masterminds\HTML5\Parser;
4
5 use WCPOS\Vendor\Masterminds\HTML5\Elements;
6 use WCPOS\Vendor\Masterminds\HTML5\InstructionProcessor;
7 /**
8 * Create an HTML5 DOM tree from events.
9 *
10 * This attempts to create a DOM from events emitted by a parser. This
11 * attempts (but does not guarantee) to up-convert older HTML documents
12 * to HTML5. It does this by applying HTML5's rules, but it will not
13 * change the architecture of the document itself.
14 *
15 * Many of the error correction and quirks features suggested in the specification
16 * are implemented herein; however, not all of them are. Since we do not
17 * assume a graphical user agent, no presentation-specific logic is conducted
18 * during tree building.
19 *
20 * FIXME: The present tree builder does not exactly follow the state machine rules
21 * for insert modes as outlined in the HTML5 spec. The processor needs to be
22 * re-written to accomodate this. See, for example, the Go language HTML5
23 * parser.
24 */
25 class DOMTreeBuilder implements EventHandler
26 {
27 /**
28 * Defined in http://www.w3.org/TR/html51/infrastructure.html#html-namespace-0.
29 */
30 const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml';
31 const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML';
32 const NAMESPACE_SVG = 'http://www.w3.org/2000/svg';
33 const NAMESPACE_XLINK = 'http://www.w3.org/1999/xlink';
34 const NAMESPACE_XML = 'http://www.w3.org/XML/1998/namespace';
35 const NAMESPACE_XMLNS = 'http://www.w3.org/2000/xmlns/';
36 const OPT_DISABLE_HTML_NS = 'disable_html_ns';
37 const OPT_TARGET_DOC = 'target_document';
38 const OPT_IMPLICIT_NS = 'implicit_namespaces';
39 /**
40 * Holds the HTML5 element names that causes a namespace switch.
41 *
42 * @var array
43 */
44 protected $nsRoots = array('html' => self::NAMESPACE_HTML, 'svg' => self::NAMESPACE_SVG, 'math' => self::NAMESPACE_MATHML);
45 /**
46 * Holds the always available namespaces (which does not require the XMLNS declaration).
47 *
48 * @var array
49 */
50 protected $implicitNamespaces = array('xml' => self::NAMESPACE_XML, 'xmlns' => self::NAMESPACE_XMLNS, 'xlink' => self::NAMESPACE_XLINK);
51 /**
52 * Holds a stack of currently active namespaces.
53 *
54 * @var array
55 */
56 protected $nsStack = array();
57 /**
58 * Holds the number of namespaces declared by a node.
59 *
60 * @var array
61 */
62 protected $pushes = array();
63 /**
64 * Defined in 8.2.5.
65 */
66 const IM_INITIAL = 0;
67 const IM_BEFORE_HTML = 1;
68 const IM_BEFORE_HEAD = 2;
69 const IM_IN_HEAD = 3;
70 const IM_IN_HEAD_NOSCRIPT = 4;
71 const IM_AFTER_HEAD = 5;
72 const IM_IN_BODY = 6;
73 const IM_TEXT = 7;
74 const IM_IN_TABLE = 8;
75 const IM_IN_TABLE_TEXT = 9;
76 const IM_IN_CAPTION = 10;
77 const IM_IN_COLUMN_GROUP = 11;
78 const IM_IN_TABLE_BODY = 12;
79 const IM_IN_ROW = 13;
80 const IM_IN_CELL = 14;
81 const IM_IN_SELECT = 15;
82 const IM_IN_SELECT_IN_TABLE = 16;
83 const IM_AFTER_BODY = 17;
84 const IM_IN_FRAMESET = 18;
85 const IM_AFTER_FRAMESET = 19;
86 const IM_AFTER_AFTER_BODY = 20;
87 const IM_AFTER_AFTER_FRAMESET = 21;
88 const IM_IN_SVG = 22;
89 const IM_IN_MATHML = 23;
90 protected $options = array();
91 protected $stack = array();
92 protected $current;
93 // Pointer in the tag hierarchy.
94 protected $rules;
95 protected $doc;
96 protected $frag;
97 protected $processor;
98 protected $insertMode = 0;
99 /**
100 * Track if we are in an element that allows only inline child nodes.
101 *
102 * @var string|null
103 */
104 protected $onlyInline;
105 /**
106 * Quirks mode is enabled by default.
107 * Any document that is missing the DT will be considered to be in quirks mode.
108 */
109 protected $quirks = \true;
110 protected $errors = array();
111 public function __construct($isFragment = \false, array $options = array())
112 {
113 $this->options = $options;
114 if (isset($options[self::OPT_TARGET_DOC])) {
115 $this->doc = $options[self::OPT_TARGET_DOC];
116 } else {
117 $impl = new \DOMImplementation();
118 // XXX:
119 // Create the doctype. For now, we are always creating HTML5
120 // documents, and attempting to up-convert any older DTDs to HTML5.
121 $dt = $impl->createDocumentType('html');
122 // $this->doc = \DOMImplementation::createDocument(NULL, 'html', $dt);
123 $this->doc = $impl->createDocument(null, '', $dt);
124 $this->doc->encoding = !empty($options['encoding']) ? $options['encoding'] : 'UTF-8';
125 }
126 $this->errors = array();
127 $this->current = $this->doc;
128 // ->documentElement;
129 // Create a rules engine for tags.
130 $this->rules = new TreeBuildingRules();
131 $implicitNS = array();
132 if (isset($this->options[self::OPT_IMPLICIT_NS])) {
133 $implicitNS = $this->options[self::OPT_IMPLICIT_NS];
134 } elseif (isset($this->options['implicitNamespaces'])) {
135 $implicitNS = $this->options['implicitNamespaces'];
136 }
137 // Fill $nsStack with the defalut HTML5 namespaces, plus the "implicitNamespaces" array taken form $options
138 \array_unshift($this->nsStack, $implicitNS + array('' => self::NAMESPACE_HTML) + $this->implicitNamespaces);
139 if ($isFragment) {
140 $this->insertMode = static::IM_IN_BODY;
141 $this->frag = $this->doc->createDocumentFragment();
142 $this->current = $this->frag;
143 }
144 }
145 /**
146 * Get the document.
147 */
148 public function document()
149 {
150 return $this->doc;
151 }
152 /**
153 * Get the DOM fragment for the body.
154 *
155 * This returns a DOMNodeList because a fragment may have zero or more
156 * DOMNodes at its root.
157 *
158 * @see http://www.w3.org/TR/2012/CR-html5-20121217/syntax.html#concept-frag-parse-context
159 *
160 * @return \DOMDocumentFragment
161 */
162 public function fragment()
163 {
164 return $this->frag;
165 }
166 /**
167 * Provide an instruction processor.
168 *
169 * This is used for handling Processor Instructions as they are
170 * inserted. If omitted, PI's are inserted directly into the DOM tree.
171 */
172 public function setInstructionProcessor(InstructionProcessor $proc)
173 {
174 $this->processor = $proc;
175 }
176 public function doctype($name, $idType = 0, $id = null, $quirks = \false)
177 {
178 // This is used solely for setting quirks mode. Currently we don't
179 // try to preserve the inbound DT. We convert it to HTML5.
180 $this->quirks = $quirks;
181 if ($this->insertMode > static::IM_INITIAL) {
182 $this->parseError('Illegal placement of DOCTYPE tag. Ignoring: ' . $name);
183 return;
184 }
185 $this->insertMode = static::IM_BEFORE_HTML;
186 }
187 /**
188 * Process the start tag.
189 *
190 * @todo - XMLNS namespace handling (we need to parse, even if it's not valid)
191 * - XLink, MathML and SVG namespace handling
192 * - Omission rules: 8.1.2.4 Optional tags
193 *
194 * @param string $name
195 * @param array $attributes
196 * @param bool $selfClosing
197 *
198 * @return int
199 */
200 public function startTag($name, $attributes = array(), $selfClosing = \false)
201 {
202 $lname = $this->normalizeTagName($name);
203 // Make sure we have an html element.
204 if (!$this->doc->documentElement && 'html' !== $name && !$this->frag) {
205 $this->startTag('html');
206 }
207 // Set quirks mode if we're at IM_INITIAL with no doctype.
208 if ($this->insertMode === static::IM_INITIAL) {
209 $this->quirks = \true;
210 $this->parseError('No DOCTYPE specified.');
211 }
212 // SPECIAL TAG HANDLING:
213 // Spec says do this, and "don't ask."
214 // find the spec where this is defined... looks problematic
215 if ('image' === $name && !($this->insertMode === static::IM_IN_SVG || $this->insertMode === static::IM_IN_MATHML)) {
216 $name = 'img';
217 }
218 // Autoclose p tags where appropriate.
219 if ($this->insertMode >= static::IM_IN_BODY && Elements::isA($name, Elements::AUTOCLOSE_P)) {
220 $this->autoclose('p');
221 }
222 // Set insert mode:
223 switch ($name) {
224 case 'html':
225 $this->insertMode = static::IM_BEFORE_HEAD;
226 break;
227 case 'head':
228 if ($this->insertMode > static::IM_BEFORE_HEAD) {
229 $this->parseError('Unexpected head tag outside of head context.');
230 } else {
231 $this->insertMode = static::IM_IN_HEAD;
232 }
233 break;
234 case 'body':
235 $this->insertMode = static::IM_IN_BODY;
236 break;
237 case 'svg':
238 $this->insertMode = static::IM_IN_SVG;
239 break;
240 case 'math':
241 $this->insertMode = static::IM_IN_MATHML;
242 break;
243 case 'noscript':
244 if ($this->insertMode === static::IM_IN_HEAD) {
245 $this->insertMode = static::IM_IN_HEAD_NOSCRIPT;
246 }
247 break;
248 }
249 // Special case handling for SVG.
250 if ($this->insertMode === static::IM_IN_SVG) {
251 $lname = Elements::normalizeSvgElement($lname);
252 }
253 $pushes = 0;
254 // when we found a tag thats appears inside $nsRoots, we have to switch the defalut namespace
255 if (isset($this->nsRoots[$lname]) && $this->nsStack[0][''] !== $this->nsRoots[$lname]) {
256 \array_unshift($this->nsStack, array('' => $this->nsRoots[$lname]) + $this->nsStack[0]);
257 ++$pushes;
258 }
259 $needsWorkaround = \false;
260 if (isset($this->options['xmlNamespaces']) && $this->options['xmlNamespaces']) {
261 // when xmlNamespaces is true a and we found a 'xmlns' or 'xmlns:*' attribute, we should add a new item to the $nsStack
262 foreach ($attributes as $aName => $aVal) {
263 if ('xmlns' === $aName) {
264 $needsWorkaround = $aVal;
265 \array_unshift($this->nsStack, array('' => $aVal) + $this->nsStack[0]);
266 ++$pushes;
267 } elseif ('xmlns' === (($pos = \strpos($aName, ':')) ? \substr($aName, 0, $pos) : '')) {
268 \array_unshift($this->nsStack, array(\substr($aName, $pos + 1) => $aVal) + $this->nsStack[0]);
269 ++$pushes;
270 }
271 }
272 }
273 if ($this->onlyInline && Elements::isA($lname, Elements::BLOCK_TAG)) {
274 $this->autoclose($this->onlyInline);
275 $this->onlyInline = null;
276 }
277 // some elements as table related tags might have optional end tags that force us to auto close multiple tags
278 // https://www.w3.org/TR/html401/struct/tables.html
279 if ($this->current instanceof \DOMElement && isset(Elements::$optionalEndElementsParentsToClose[$lname])) {
280 foreach (Elements::$optionalEndElementsParentsToClose[$lname] as $parentElName) {
281 if ($this->current instanceof \DOMElement && $this->current->tagName === $parentElName) {
282 $this->autoclose($parentElName);
283 }
284 }
285 }
286 try {
287 $prefix = ($pos = \strpos($lname, ':')) ? \substr($lname, 0, $pos) : '';
288 if (\false !== $needsWorkaround) {
289 $xml = "<{$lname} xmlns=\"{$needsWorkaround}\" " . (\strlen($prefix) && isset($this->nsStack[0][$prefix]) ? "xmlns:{$prefix}=\"" . $this->nsStack[0][$prefix] . '"' : '') . '/>';
290 $frag = new \DOMDocument('1.0', 'UTF-8');
291 $frag->loadXML($xml);
292 $ele = $this->doc->importNode($frag->documentElement, \true);
293 } else {
294 if (!isset($this->nsStack[0][$prefix]) || '' === $prefix && isset($this->options[self::OPT_DISABLE_HTML_NS]) && $this->options[self::OPT_DISABLE_HTML_NS]) {
295 $ele = $this->doc->createElement($lname);
296 } else {
297 $ele = $this->doc->createElementNS($this->nsStack[0][$prefix], $lname);
298 }
299 }
300 } catch (\DOMException $e) {
301 $this->parseError("Illegal tag name: <{$lname}>. Replaced with <invalid>.");
302 $ele = $this->doc->createElement('invalid');
303 }
304 if (Elements::isA($lname, Elements::BLOCK_ONLY_INLINE)) {
305 $this->onlyInline = $lname;
306 }
307 // When we add some namespacess, we have to track them. Later, when "endElement" is invoked, we have to remove them.
308 // When we are on a void tag, we do not need to care about namesapce nesting.
309 if ($pushes > 0 && !Elements::isA($name, Elements::VOID_TAG)) {
310 // PHP tends to free the memory used by DOM,
311 // to avoid spl_object_id collisions we have to avoid garbage collection of $ele storing it into $pushes
312 // see https://bugs.php.net/bug.php?id=67459
313 $this->pushes[\spl_object_id($ele)] = array($pushes, $ele);
314 }
315 foreach ($attributes as $aName => $aVal) {
316 // xmlns attributes can't be set
317 if ('xmlns' === $aName) {
318 continue;
319 }
320 if ($this->insertMode === static::IM_IN_SVG) {
321 $aName = Elements::normalizeSvgAttribute($aName);
322 } elseif ($this->insertMode === static::IM_IN_MATHML) {
323 $aName = Elements::normalizeMathMlAttribute($aName);
324 }
325 $aVal = (string) $aVal;
326 try {
327 $prefix = ($pos = \strpos($aName, ':')) ? \substr($aName, 0, $pos) : \false;
328 if ('xmlns' === $prefix) {
329 $ele->setAttributeNS(self::NAMESPACE_XMLNS, $aName, $aVal);
330 } elseif (\false !== $prefix && isset($this->nsStack[0][$prefix])) {
331 $ele->setAttributeNS($this->nsStack[0][$prefix], $aName, $aVal);
332 } else {
333 $ele->setAttribute($aName, $aVal);
334 }
335 } catch (\DOMException $e) {
336 $this->parseError("Illegal attribute name for tag {$name}. Ignoring: {$aName}");
337 continue;
338 }
339 // This is necessary on a non-DTD schema, like HTML5.
340 if ('id' === $aName) {
341 $ele->setIdAttribute('id', \true);
342 }
343 }
344 if ($this->frag !== $this->current && $this->rules->hasRules($name)) {
345 // Some elements have special processing rules. Handle those separately.
346 $this->current = $this->rules->evaluate($ele, $this->current);
347 } else {
348 // Otherwise, it's a standard element.
349 $this->current->appendChild($ele);
350 if (!Elements::isA($name, Elements::VOID_TAG)) {
351 $this->current = $ele;
352 }
353 // Self-closing tags should only be respected on foreign elements
354 // (and are implied on void elements)
355 // See: https://www.w3.org/TR/html5/syntax.html#start-tags
356 if (Elements::isHtml5Element($name)) {
357 $selfClosing = \false;
358 }
359 }
360 // This is sort of a last-ditch attempt to correct for cases where no head/body
361 // elements are provided.
362 if ($this->insertMode <= static::IM_BEFORE_HEAD && 'head' !== $name && 'html' !== $name) {
363 $this->insertMode = static::IM_IN_BODY;
364 }
365 // When we are on a void tag, we do not need to care about namesapce nesting,
366 // but we have to remove the namespaces pushed to $nsStack.
367 if ($pushes > 0 && Elements::isA($name, Elements::VOID_TAG)) {
368 // remove the namespaced definded by current node
369 for ($i = 0; $i < $pushes; ++$i) {
370 \array_shift($this->nsStack);
371 }
372 }
373 if ($selfClosing) {
374 $this->endTag($name);
375 }
376 // Return the element mask, which the tokenizer can then use to set
377 // various processing rules.
378 return Elements::element($name);
379 }
380 public function endTag($name)
381 {
382 $lname = $this->normalizeTagName($name);
383 // Special case within 12.2.6.4.7: An end tag whose tag name is "br" should be treated as an opening tag
384 if ('br' === $name) {
385 $this->parseError('Closing tag encountered for void element br.');
386 $this->startTag('br');
387 } elseif (Elements::isA($name, Elements::VOID_TAG)) {
388 return;
389 }
390 if ($this->insertMode <= static::IM_BEFORE_HTML) {
391 // 8.2.5.4.2
392 if (\in_array($name, array('html', 'br', 'head', 'title'))) {
393 $this->startTag('html');
394 $this->endTag($name);
395 $this->insertMode = static::IM_BEFORE_HEAD;
396 return;
397 }
398 // Ignore the tag.
399 $this->parseError('Illegal closing tag at global scope.');
400 return;
401 }
402 // Special case handling for SVG.
403 if ($this->insertMode === static::IM_IN_SVG) {
404 $lname = Elements::normalizeSvgElement($lname);
405 }
406 $cid = \spl_object_id($this->current);
407 // XXX: HTML has no parent. What do we do, though,
408 // if this element appears in the wrong place?
409 if ('html' === $lname) {
410 return;
411 }
412 // remove the namespaced definded by current node
413 if (isset($this->pushes[$cid])) {
414 for ($i = 0; $i < $this->pushes[$cid][0]; ++$i) {
415 \array_shift($this->nsStack);
416 }
417 unset($this->pushes[$cid]);
418 }
419 if (!$this->autoclose($lname)) {
420 $this->parseError('Could not find closing tag for ' . $lname);
421 }
422 switch ($lname) {
423 case 'head':
424 $this->insertMode = static::IM_AFTER_HEAD;
425 break;
426 case 'body':
427 $this->insertMode = static::IM_AFTER_BODY;
428 break;
429 case 'svg':
430 case 'mathml':
431 $this->insertMode = static::IM_IN_BODY;
432 break;
433 }
434 }
435 public function comment($cdata)
436 {
437 // TODO: Need to handle case where comment appears outside of the HTML tag.
438 $node = $this->doc->createComment($cdata);
439 $this->current->appendChild($node);
440 }
441 public function text($data)
442 {
443 // XXX: Hmmm.... should we really be this strict?
444 if ($this->insertMode < static::IM_IN_HEAD) {
445 // Per '8.2.5.4.3 The "before head" insertion mode' the characters
446 // " \t\n\r\f" should be ignored but no mention of a parse error. This is
447 // practical as most documents contain these characters. Other text is not
448 // expected here so recording a parse error is necessary.
449 $dataTmp = \trim($data, " \t\n\r\f");
450 if (!empty($dataTmp)) {
451 // fprintf(STDOUT, "Unexpected insert mode: %d", $this->insertMode);
452 $this->parseError('Unexpected text. Ignoring: ' . $dataTmp);
453 }
454 return;
455 }
456 // fprintf(STDOUT, "Appending text %s.", $data);
457 $node = $this->doc->createTextNode($data);
458 $this->current->appendChild($node);
459 }
460 public function eof()
461 {
462 // If the $current isn't the $root, do we need to do anything?
463 }
464 public function parseError($msg, $line = 0, $col = 0)
465 {
466 $this->errors[] = \sprintf('Line %d, Col %d: %s', $line, $col, $msg);
467 }
468 public function getErrors()
469 {
470 return $this->errors;
471 }
472 public function cdata($data)
473 {
474 $node = $this->doc->createCDATASection($data);
475 $this->current->appendChild($node);
476 }
477 public function processingInstruction($name, $data = null)
478 {
479 // XXX: Ignore initial XML declaration, per the spec.
480 if ($this->insertMode === static::IM_INITIAL && 'xml' === \strtolower($name)) {
481 return;
482 }
483 // Important: The processor may modify the current DOM tree however it sees fit.
484 if ($this->processor instanceof InstructionProcessor) {
485 $res = $this->processor->process($this->current, $name, $data);
486 if (!empty($res)) {
487 $this->current = $res;
488 }
489 return;
490 }
491 // Otherwise, this is just a dumb PI element.
492 $node = $this->doc->createProcessingInstruction($name, $data);
493 $this->current->appendChild($node);
494 }
495 // ==========================================================================
496 // UTILITIES
497 // ==========================================================================
498 /**
499 * Apply normalization rules to a tag name.
500 * See sections 2.9 and 8.1.2.
501 *
502 * @param string $tagName
503 *
504 * @return string The normalized tag name.
505 */
506 protected function normalizeTagName($tagName)
507 {
508 /*
509 * Section 2.9 suggests that we should not do this. if (strpos($name, ':') !== false) { // We know from the grammar that there must be at least one other // char besides :, since : is not a legal tag start. $parts = explode(':', $name); return array_pop($parts); }
510 */
511 return $tagName;
512 }
513 protected function quirksTreeResolver($name)
514 {
515 throw new \Exception('Not implemented.');
516 }
517 /**
518 * Automatically climb the tree and close the closest node with the matching $tag.
519 *
520 * @param string $tagName
521 *
522 * @return bool
523 */
524 protected function autoclose($tagName)
525 {
526 $working = $this->current;
527 do {
528 if (\XML_ELEMENT_NODE !== $working->nodeType) {
529 return \false;
530 }
531 if ($working->tagName === $tagName) {
532 $this->current = $working->parentNode;
533 return \true;
534 }
535 } while ($working = $working->parentNode);
536 return \false;
537 }
538 /**
539 * Checks if the given tagname is an ancestor of the present candidate.
540 *
541 * If $this->current or anything above $this->current matches the given tag
542 * name, this returns true.
543 *
544 * @param string $tagName
545 *
546 * @return bool
547 */
548 protected function isAncestor($tagName)
549 {
550 $candidate = $this->current;
551 while (\XML_ELEMENT_NODE === $candidate->nodeType) {
552 if ($candidate->tagName === $tagName) {
553 return \true;
554 }
555 $candidate = $candidate->parentNode;
556 }
557 return \false;
558 }
559 /**
560 * Returns true if the immediate parent element is of the given tagname.
561 *
562 * @param string $tagName
563 *
564 * @return bool
565 */
566 protected function isParent($tagName)
567 {
568 return $this->current->tagName === $tagName;
569 }
570 }
571