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