1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.html.parser.neko;
16
17 import static org.htmlunit.BrowserVersionFeatures.JS_SCRIPT_IN_TEMPLATE_EXECUTED_ON_ATTACH;
18
19 import java.io.IOException;
20 import java.io.StringReader;
21 import java.net.URL;
22 import java.nio.charset.Charset;
23 import java.util.ArrayDeque;
24 import java.util.Deque;
25
26 import org.htmlunit.BrowserVersion;
27 import org.htmlunit.ObjectInstantiationException;
28 import org.htmlunit.WebClient;
29 import org.htmlunit.WebResponse;
30 import org.htmlunit.cyberneko.HTMLConfiguration;
31 import org.htmlunit.cyberneko.HTMLElements;
32 import org.htmlunit.cyberneko.HTMLScanner;
33 import org.htmlunit.cyberneko.HTMLTagBalancingListener;
34 import org.htmlunit.cyberneko.xerces.parsers.AbstractSAXParser;
35 import org.htmlunit.cyberneko.xerces.xni.Augmentations;
36 import org.htmlunit.cyberneko.xerces.xni.QName;
37 import org.htmlunit.cyberneko.xerces.xni.XMLAttributes;
38 import org.htmlunit.cyberneko.xerces.xni.XMLString;
39 import org.htmlunit.cyberneko.xerces.xni.XNIException;
40 import org.htmlunit.cyberneko.xerces.xni.parser.XMLInputSource;
41 import org.htmlunit.cyberneko.xerces.xni.parser.XMLParserConfiguration;
42 import org.htmlunit.html.DomCDataSection;
43 import org.htmlunit.html.DomComment;
44 import org.htmlunit.html.DomDocumentType;
45 import org.htmlunit.html.DomElement;
46 import org.htmlunit.html.DomNode;
47 import org.htmlunit.html.DomText;
48 import org.htmlunit.html.ElementFactory;
49 import org.htmlunit.html.Html;
50 import org.htmlunit.html.HtmlBody;
51 import org.htmlunit.html.HtmlElement;
52 import org.htmlunit.html.HtmlForm;
53 import org.htmlunit.html.HtmlHiddenInput;
54 import org.htmlunit.html.HtmlImage;
55 import org.htmlunit.html.HtmlPage;
56 import org.htmlunit.html.HtmlSvg;
57 import org.htmlunit.html.HtmlTable;
58 import org.htmlunit.html.HtmlTableRow;
59 import org.htmlunit.html.HtmlTemplate;
60 import org.htmlunit.html.ScriptElement;
61 import org.htmlunit.html.SubmittableElement;
62 import org.htmlunit.html.UnknownElementFactory;
63 import org.htmlunit.html.XHtmlPage;
64 import org.htmlunit.html.parser.HTMLParser;
65 import org.htmlunit.html.parser.HTMLParserDOMBuilder;
66 import org.htmlunit.html.parser.HTMLParserListener;
67 import org.htmlunit.javascript.host.html.HTMLBodyElement;
68 import org.htmlunit.svg.SvgElementFactory;
69 import org.htmlunit.util.StringUtils;
70 import org.w3c.dom.Node;
71 import org.xml.sax.Attributes;
72 import org.xml.sax.ContentHandler;
73 import org.xml.sax.Locator;
74 import org.xml.sax.SAXException;
75 import org.xml.sax.ext.LexicalHandler;
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97 final class HtmlUnitNekoDOMBuilder extends AbstractSAXParser
98 implements ContentHandler, LexicalHandler, HTMLTagBalancingListener, HTMLParserDOMBuilder {
99
100
101 private static final HTMLElements HTMLELEMENTS = new HTMLElements();
102
103 private enum HeadParsed { YES, SYNTHESIZED, NO }
104
105 private final HTMLParser htmlParser_;
106 private final HtmlPage page_;
107
108 private Locator locator_;
109 private final Deque<DomNode> stack_ = new ArrayDeque<>();
110
111
112 private boolean snippetStartNodeOverwritten_;
113 private final int initialSize_;
114 private DomNode currentNode_;
115 private final boolean createdByJavascript_;
116 private final XMLString characters_ = new XMLString();
117 private HtmlUnitNekoDOMBuilder.HeadParsed headParsed_ = HeadParsed.NO;
118 private HtmlElement body_;
119 private boolean lastTagWasSynthesized_;
120 private HtmlForm consumingForm_;
121 private boolean formEndingIsAdjusting_;
122 private boolean insideSvg_;
123 private boolean insideTemplate_;
124
125 private static final String FEATURE_AUGMENTATIONS = "http://cyberneko.org/html/features/augmentations";
126 private static final String FEATURE_PARSE_NOSCRIPT
127 = "http://cyberneko.org/html/features/parse-noscript-content";
128
129
130
131
132
133 @Override
134 public void pushInputString(final String html) {
135 page_.registerParsingStart();
136 page_.registerInlineSnippetParsingStart();
137 try {
138 final WebResponse webResponse = page_.getWebResponse();
139 final Charset charset = webResponse.getContentCharset();
140 final String url = webResponse.getWebRequest().getUrl().toString();
141 final XMLInputSource in = new XMLInputSource(null, url, null, new StringReader(html), charset.name());
142 ((HTMLConfiguration) parserConfiguration_).evaluateInputSource(in);
143 }
144 finally {
145 page_.registerParsingEnd();
146 page_.registerInlineSnippetParsingEnd();
147 }
148 }
149
150
151
152
153
154
155
156 HtmlUnitNekoDOMBuilder(final HTMLParser htmlParser, final WebClient webClient,
157 final DomNode node, final URL url, final String htmlContent, final boolean createdByJavascript) {
158 super(createConfiguration(webClient.getBrowserVersion()));
159
160 htmlParser_ = htmlParser;
161 page_ = (HtmlPage) node.getPage();
162
163 currentNode_ = node;
164 for (final Node ancestor : currentNode_.getAncestors()) {
165 stack_.push((DomNode) ancestor);
166 }
167 createdByJavascript_ = createdByJavascript;
168
169 final HTMLParserListener listener = webClient.getHTMLParserListener();
170 final boolean reportErrors = listener != null;
171 if (reportErrors) {
172 parserConfiguration_.setErrorHandler(new HtmlUnitNekoHTMLErrorHandler(listener, url, htmlContent));
173 }
174
175 try {
176 setFeature(FEATURE_AUGMENTATIONS, true);
177 setFeature("http://cyberneko.org/html/features/report-errors", reportErrors);
178 setFeature(FEATURE_PARSE_NOSCRIPT, !webClient.isJavaScriptEnabled());
179 setFeature(HTMLScanner.ALLOW_SELFCLOSING_IFRAME, false);
180
181 setContentHandler(this);
182 setLexicalHandler(this);
183 }
184 catch (final SAXException e) {
185 throw new ObjectInstantiationException("unable to create HTML parser", e);
186 }
187 initialSize_ = stack_.size();
188 }
189
190
191
192
193
194
195
196 void setInsideSvg(final boolean insideSvg) {
197 insideSvg_ = insideSvg;
198 }
199
200
201
202
203
204 private static XMLParserConfiguration createConfiguration(final BrowserVersion browserVersion) {
205
206
207
208 return new HTMLConfiguration(new HTMLElements.HTMLElementsWithCache(HTMLELEMENTS));
209 }
210
211
212
213
214 @Override
215 public void setDocumentLocator(final Locator locator) {
216 locator_ = locator;
217 }
218
219
220
221
222 @Override
223 public void startDocument() throws SAXException {
224
225 }
226
227
228 @Override
229 public void startElement(final QName element, final XMLAttributes attributes, final Augmentations augs)
230 throws XNIException {
231
232 lastTagWasSynthesized_ = augs.isSynthesized();
233 super.startElement(element, attributes, augs);
234 }
235
236
237
238
239 @Override
240 public void startElement(String namespaceURI, final String localName, final String qName, final Attributes atts)
241 throws SAXException {
242
243 if (snippetStartNodeOverwritten_) {
244 snippetStartNodeOverwritten_ = false;
245 return;
246 }
247 handleCharacters();
248
249 final String tagLower = StringUtils.toRootLowerCase(localName);
250 if (page_.isParsingHtmlSnippet() && ("html".equals(tagLower) || "body".equals(tagLower))) {
251
252
253 stack_.push(currentNode_);
254 return;
255 }
256
257 if ("head".equals(tagLower)) {
258 if (headParsed_ == HeadParsed.YES || page_.isParsingHtmlSnippet()) {
259
260
261 stack_.push(currentNode_);
262 return;
263 }
264
265 headParsed_ = lastTagWasSynthesized_ ? HeadParsed.SYNTHESIZED : HeadParsed.YES;
266 }
267
268
269
270 HtmlBody oldBody = null;
271 final boolean isBodyTag = "body".equals(tagLower);
272 if (isBodyTag) {
273 final HtmlBody body = page_.getBody();
274 if (body != null) {
275 oldBody = body;
276 }
277 }
278
279 if (namespaceURI != null) {
280 namespaceURI = namespaceURI.trim();
281 }
282
283 if (!(page_ instanceof XHtmlPage) && Html.XHTML_NAMESPACE.equals(namespaceURI)) {
284 namespaceURI = null;
285 }
286
287 ElementFactory factory =
288 htmlParser_.getElementFactory(page_, namespaceURI, qName, insideSvg_, false);
289
290
291
292 if (page_ instanceof XHtmlPage
293 && !insideSvg_
294 && "svg".equals(tagLower)
295 && !Html.SVG_NAMESPACE.equals(namespaceURI)
296 && !Html.SVG_NAMESPACE.equals(atts.getValue("xmlns"))) {
297 factory = UnknownElementFactory.INSTANCE;
298 }
299
300 String elementName = qName;
301 if (factory == HtmlUnitNekoHtmlParser.SVG_FACTORY) {
302 namespaceURI = Html.SVG_NAMESPACE;
303
304
305
306 if (!(page_ instanceof XHtmlPage)) {
307 elementName = SvgElementFactory.adjustTagNameForHtmlParser(qName);
308 }
309 }
310
311 final DomElement newElement = factory.createElementNS(page_, namespaceURI, elementName, atts);
312 newElement.setStartLocation(locator_.getLineNumber(), locator_.getColumnNumber());
313
314
315 addNodeToRightParent(currentNode_, newElement);
316
317 if (newElement instanceof HtmlSvg) {
318 insideSvg_ = true;
319 }
320 else if (newElement instanceof HtmlTemplate) {
321 insideTemplate_ = true;
322 }
323
324
325
326 else if (newElement instanceof HtmlForm form) {
327 consumingForm_ = form;
328 formEndingIsAdjusting_ = false;
329 }
330 else if (consumingForm_ != null) {
331
332 if (newElement instanceof SubmittableElement) {
333
334 if (((HtmlElement) newElement).getEnclosingForm() != consumingForm_) {
335 ((HtmlElement) newElement).setOwningForm(consumingForm_);
336 }
337 }
338 }
339
340
341
342 if (oldBody != null) {
343 oldBody.quietlyRemoveAndMoveChildrenTo(newElement);
344 }
345
346 if (!insideSvg_ && isBodyTag) {
347 body_ = (HtmlElement) newElement;
348 }
349 else if (createdByJavascript_
350 && newElement instanceof ScriptElement script
351 && (!insideTemplate_
352 || !page_.getWebClient().getBrowserVersion()
353 .hasFeature(JS_SCRIPT_IN_TEMPLATE_EXECUTED_ON_ATTACH))) {
354 script.markAsCreatedByDomParser();
355 }
356
357 currentNode_ = newElement;
358 stack_.push(currentNode_);
359 }
360
361
362
363
364
365 private void addNodeToRightParent(final DomNode currentNode, final DomElement newElement) {
366 final String currentNodeName = currentNode.getNodeName();
367 final String newNodeName = newElement.getNodeName();
368
369
370 if (isTableChild(newNodeName)) {
371 final DomNode parent =
372 "table".equals(currentNodeName) ? currentNode : findElementOnStack("table");
373 appendChild(parent, newElement);
374 return;
375 }
376 if ("tr".equals(newNodeName)) {
377 final DomNode parent =
378 isTableChild(currentNodeName) ? currentNode : findElementOnStack("tbody", "thead", "tfoot");
379 appendChild(parent, newElement);
380 return;
381 }
382 if (isTableCell(newNodeName)) {
383 final DomNode parent =
384 "tr".equals(currentNodeName) ? currentNode : findElementOnStack("tr");
385 appendChild(parent, newElement);
386 return;
387 }
388
389
390 if ("table".equals(currentNodeName) || isTableChild(currentNodeName) || "tr".equals(currentNodeName)) {
391 if ("template".equals(newNodeName)) {
392 currentNode.appendChild(newElement);
393 }
394
395
396 else if (!"colgroup".equals(currentNodeName)
397 && ("script".equals(newNodeName)
398 || "form".equals(newNodeName)
399 || "style".equals(newNodeName))) {
400 currentNode.appendChild(newElement);
401 }
402
403
404 else if ("col".equals(newNodeName) && "colgroup".equals(currentNodeName)) {
405 currentNode.appendChild(newElement);
406 }
407 else if ("caption".equals(currentNodeName)) {
408 currentNode.appendChild(newElement);
409 }
410 else if (newElement instanceof HtmlHiddenInput) {
411 currentNode.appendChild(newElement);
412 }
413 else {
414
415 final DomNode parent = findElementOnStack("table");
416 parent.insertBefore(newElement);
417 }
418 return;
419 }
420
421 if (formEndingIsAdjusting_ && "form".equals(currentNodeName)) {
422
423 appendChild(currentNode.getParentNode(), newElement);
424 return;
425 }
426
427
428 appendChild(currentNode, newElement);
429 }
430
431 private DomNode findElementOnStack(final String searchedElementName) {
432 for (final DomNode node : stack_) {
433 if (searchedElementName.equals(node.getNodeName())) {
434 return node;
435 }
436 }
437
438
439 return stack_.peek();
440 }
441
442 private DomNode findElementOnStack(final String... searchedElementNames) {
443 for (final DomNode node : stack_) {
444 for (final String searchedElementName : searchedElementNames) {
445 if (searchedElementName.equals(node.getNodeName())) {
446 return node;
447 }
448 }
449 }
450
451
452 return stack_.peek();
453 }
454
455 private static boolean isTableChild(final String nodeName) {
456 if (nodeName == null || nodeName.length() < 5) {
457 return false;
458 }
459
460 return "thead".equals(nodeName)
461 || "tbody".equals(nodeName)
462 || "tfoot".equals(nodeName)
463 || "caption".equals(nodeName)
464 || "colgroup".equals(nodeName);
465 }
466
467 private static boolean isTableCell(final String nodeName) {
468 if (nodeName == null || nodeName.length() != 2) {
469 return false;
470 }
471 return "td".equals(nodeName) || "th".equals(nodeName);
472 }
473
474
475 @Override
476 public void endElement(final QName element, final Augmentations augs)
477 throws XNIException {
478
479 lastTagWasSynthesized_ = augs.isSynthesized();
480 super.endElement(element, augs);
481 }
482
483
484
485
486 @Override
487 public void endElement(final String namespaceURI, final String localName, final String qName)
488 throws SAXException {
489
490 final String tagLower = StringUtils.toRootLowerCase(localName);
491
492 handleCharacters();
493
494 if (page_.isParsingHtmlSnippet()) {
495 if ("html".equals(tagLower) || "body".equals(tagLower)) {
496 return;
497 }
498 if (stack_.size() == initialSize_) {
499
500
501 snippetStartNodeOverwritten_ = !StringUtils.equalsChar('p', tagLower);
502 return;
503 }
504 }
505
506 if ("svg".equals(tagLower)) {
507 insideSvg_ = false;
508 }
509 else if ("template".equals(tagLower)) {
510 insideTemplate_ = false;
511 }
512
513
514
515
516 if (stack_.isEmpty()) {
517 return;
518 }
519
520 final DomNode previousNode = stack_.pop();
521 previousNode.setEndLocation(locator_.getLineNumber(), locator_.getColumnNumber());
522
523 if ("form".equals(tagLower) && !lastTagWasSynthesized_) {
524
525
526 consumingForm_ = null;
527 }
528
529 if (!stack_.isEmpty()) {
530 currentNode_ = stack_.peek();
531 }
532
533 final boolean postponed = page_.isParsingInlineHtmlSnippet();
534 previousNode.onAllChildrenAddedToPage(postponed);
535 }
536
537
538 @Override
539 public void characters(final char[] ch, final int start, final int length) throws SAXException {
540 characters_.append(ch, start, length);
541 }
542
543
544 @Override
545 public void ignorableWhitespace(final char[] ch, final int start, final int length) throws SAXException {
546 characters_.append(ch, start, length);
547 }
548
549
550
551
552 private void handleCharacters() {
553
554 if (characters_.length() == 0) {
555 return;
556 }
557
558
559 final String textValue = characters_.toString();
560 characters_.clear();
561
562 if (StringUtils.isBlank(textValue)) {
563 appendChild(currentNode_, new DomText(page_, textValue));
564 return;
565 }
566
567
568 if (currentNode_ instanceof HtmlTableRow row) {
569 final HtmlTable enclosingTable = row.getEnclosingTable();
570 if (enclosingTable != null) {
571 if (enclosingTable.getPreviousSibling() instanceof DomText domText) {
572 domText.setTextContent(domText.getWholeText() + textValue);
573 }
574 else {
575 enclosingTable.insertBefore(new DomText(page_, textValue));
576 }
577 }
578 }
579 else if (currentNode_ instanceof HtmlTable enclosingTable) {
580 if (enclosingTable.getPreviousSibling() instanceof DomText domText) {
581 domText.setTextContent(domText.getWholeText() + textValue);
582 }
583 else {
584 enclosingTable.insertBefore(new DomText(page_, textValue));
585 }
586 }
587 else if (currentNode_ instanceof HtmlImage) {
588 currentNode_.getParentNode().appendChild(new DomText(page_, textValue));
589 }
590 else {
591 appendChild(currentNode_, new DomText(page_, textValue));
592 }
593 }
594
595
596 @Override
597 public void endDocument() throws SAXException {
598 handleCharacters();
599 if (locator_ != null) {
600 page_.setEndLocation(locator_.getLineNumber(), locator_.getColumnNumber());
601 }
602 }
603
604
605 @Override
606 public void startPrefixMapping(final String prefix, final String uri) throws SAXException {
607
608 }
609
610
611 @Override
612 public void endPrefixMapping(final String prefix) throws SAXException {
613
614 }
615
616
617 @Override
618 public void processingInstruction(final String target, final String data) throws SAXException {
619
620 }
621
622
623 @Override
624 public void skippedEntity(final String name) throws SAXException {
625
626 }
627
628
629
630
631 @Override
632 public void comment(final char[] ch, final int start, final int length) {
633 handleCharacters();
634 final String data = new String(ch, start, length);
635 final DomComment comment = new DomComment(page_, data);
636 appendChild(currentNode_, comment);
637 }
638
639
640 @Override
641 public void endCDATA() {
642 final String data = characters_.toString();
643 characters_.clear();
644
645 final DomCDataSection cdataSection = new DomCDataSection(page_, data);
646 appendChild(currentNode_, cdataSection);
647 }
648
649
650 @Override
651 public void endDTD() {
652
653 }
654
655
656 @Override
657 public void endEntity(final String name) {
658
659 }
660
661
662 @Override
663 public void startCDATA() {
664 handleCharacters();
665 }
666
667
668 @Override
669 public void startDTD(final String name, final String publicId, final String systemId) {
670 final DomDocumentType type = new DomDocumentType(page_, name, publicId, systemId);
671 page_.setDocumentType(type);
672
673 final Node child;
674 child = type;
675 page_.appendChild(child);
676 }
677
678
679 @Override
680 public void startEntity(final String name) {
681
682 }
683
684
685
686
687 @Override
688 public void ignoredEndElement(final QName element, final Augmentations augs) {
689
690
691 if ("form".equals(element.getLocalpart()) && consumingForm_ != null) {
692 consumingForm_ = null;
693
694 if (findElementOnStack("table", "form") instanceof HtmlTable) {
695
696 }
697 else {
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725 formEndingIsAdjusting_ = true;
726 }
727 }
728 }
729
730
731
732
733 @Override
734 public void ignoredStartElement(final QName elem, final XMLAttributes attrs, final Augmentations augs) {
735
736
737 if (attrs != null && body_ != null) {
738 final String lp = elem.getLocalpart();
739 if (lp != null && lp.length() == 4) {
740 if ("body".equalsIgnoreCase(lp)) {
741 copyAttributes(body_, attrs);
742 }
743 else if ("html".equalsIgnoreCase(lp)) {
744 final DomNode parent = body_.getParentNode();
745 if (parent instanceof DomElement element) {
746 copyAttributes(element, attrs);
747 }
748 }
749 }
750 }
751 }
752
753 private static void copyAttributes(final DomElement to, final XMLAttributes attrs) {
754 final int length = attrs.getLength();
755
756 for (int i = 0; i < length; i++) {
757 final String attrName = StringUtils.toRootLowerCase(attrs.getLocalName(i));
758 if (to.getAttributes().getNamedItem(attrName) == null) {
759 to.setAttribute(attrName, attrs.getValue(i));
760 if (attrName.startsWith("on") && to.getPage().getWebClient().isJavaScriptEngineEnabled()
761 && to.getScriptableObject() instanceof HTMLBodyElement) {
762 final HTMLBodyElement jsBody = to.getScriptableObject();
763 jsBody.createEventHandlerFromAttribute(attrName, attrs.getValue(i));
764 }
765 }
766 }
767 }
768
769
770
771
772 @Override
773 public void parse(final XMLInputSource inputSource) throws XNIException, IOException {
774 final HTMLParserDOMBuilder oldBuilder = page_.getDOMBuilder();
775 page_.setDOMBuilder(this);
776 try {
777 super.parse(inputSource);
778 }
779 finally {
780 page_.setDOMBuilder(oldBuilder);
781 }
782 }
783
784 private static void appendChild(final DomNode parent, final DomNode child) {
785 if (parent instanceof HtmlTemplate template) {
786 template.getContent().appendChild(child);
787 return;
788 }
789
790 parent.appendChild(child);
791 }
792 }