1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.host.html;
16
17 import static org.htmlunit.BrowserVersionFeatures.HTMLDOCUMENT_ELEMENTS_BY_NAME_EMPTY;
18 import static org.htmlunit.javascript.configuration.SupportedBrowser.FF;
19 import static org.htmlunit.javascript.configuration.SupportedBrowser.FF_ESR;
20
21 import java.io.IOException;
22 import java.io.Serializable;
23 import java.net.URL;
24 import java.util.ArrayList;
25 import java.util.List;
26 import java.util.function.Supplier;
27
28 import org.apache.commons.lang3.StringUtils;
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31 import org.htmlunit.ScriptResult;
32 import org.htmlunit.StringWebResponse;
33 import org.htmlunit.WebClient;
34 import org.htmlunit.WebWindow;
35 import org.htmlunit.corejs.javascript.Context;
36 import org.htmlunit.corejs.javascript.Function;
37 import org.htmlunit.corejs.javascript.Scriptable;
38 import org.htmlunit.corejs.javascript.VarScope;
39 import org.htmlunit.html.BaseFrameElement;
40 import org.htmlunit.html.DomDocumentFragment;
41 import org.htmlunit.html.DomElement;
42 import org.htmlunit.html.DomNode;
43 import org.htmlunit.html.FrameWindow;
44 import org.htmlunit.html.HtmlAttributeChangeEvent;
45 import org.htmlunit.html.HtmlElement;
46 import org.htmlunit.html.HtmlForm;
47 import org.htmlunit.html.HtmlImage;
48 import org.htmlunit.html.HtmlPage;
49 import org.htmlunit.html.HtmlScript;
50 import org.htmlunit.javascript.HtmlUnitScriptable;
51 import org.htmlunit.javascript.JavaScriptEngine;
52 import org.htmlunit.javascript.PostponedAction;
53 import org.htmlunit.javascript.configuration.JsxClass;
54 import org.htmlunit.javascript.configuration.JsxConstructor;
55 import org.htmlunit.javascript.configuration.JsxFunction;
56 import org.htmlunit.javascript.configuration.JsxGetter;
57 import org.htmlunit.javascript.configuration.JsxStaticFunction;
58 import org.htmlunit.javascript.host.Element;
59 import org.htmlunit.javascript.host.dom.AbstractList.EffectOnCache;
60 import org.htmlunit.javascript.host.dom.Attr;
61 import org.htmlunit.javascript.host.dom.Document;
62 import org.htmlunit.javascript.host.dom.Node;
63 import org.htmlunit.javascript.host.dom.NodeList;
64 import org.htmlunit.javascript.host.dom.Selection;
65 import org.htmlunit.javascript.host.event.Event;
66 import org.htmlunit.util.UrlUtils;
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92 @JsxClass
93 public class HTMLDocument extends Document {
94
95 private static final Log LOG = LogFactory.getLog(HTMLDocument.class);
96
97 private enum ParsingStatus { OUTSIDE, START, IN_NAME, INSIDE, IN_STRING }
98
99
100 private final StringBuilder writeBuilder_ = new StringBuilder();
101 private boolean writeInCurrentDocument_ = true;
102
103 private boolean closePostponedAction_;
104 private boolean executionExternalPostponed_;
105
106
107
108
109 @Override
110 @JsxConstructor
111 public void jsConstructor() {
112 super.jsConstructor();
113 }
114
115
116
117
118 @Override
119 public DomNode getDomNodeOrDie() {
120 try {
121 return super.getDomNodeOrDie();
122 }
123 catch (final IllegalStateException e) {
124 throw JavaScriptEngine.typeError("No node attached to this object");
125 }
126 }
127
128
129
130
131
132 @Override
133 public HtmlPage getPage() {
134 return (HtmlPage) getDomNodeOrDie();
135 }
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150 @JsxStaticFunction
151 public static HTMLDocument parseHTMLUnsafe(final Context cx, final VarScope scope,
152 final Scriptable thisObj, final Object[] args, final Function funObj) {
153 return (HTMLDocument) Document.parseHTMLUnsafe(cx, scope, thisObj, args, funObj);
154 }
155
156
157
158
159
160
161
162
163
164
165 @JsxFunction
166 public static void write(final Context context, final VarScope scope,
167 final Scriptable thisObj, final Object[] args, final Function function) {
168 final HTMLDocument thisAsDocument = getDocument(thisObj);
169 thisAsDocument.write(concatArgsAsString(args));
170 }
171
172
173
174
175
176
177 private static String concatArgsAsString(final Object[] args) {
178 final StringBuilder builder = new StringBuilder();
179 for (final Object arg : args) {
180 builder.append(JavaScriptEngine.toString(arg));
181 }
182 return builder.toString();
183 }
184
185
186
187
188
189
190
191
192
193
194 @JsxFunction
195 public static void moveBefore(final Context context, final VarScope scope,
196 final Scriptable thisObj, final Object[] args, final Function function) {
197 Node.moveBefore(context, scope, thisObj, args, function);
198 }
199
200
201
202
203
204
205
206
207
208
209 @JsxFunction
210 public static void writeln(final Context context, final VarScope scope,
211 final Scriptable thisObj, final Object[] args, final Function function) {
212 final HTMLDocument thisAsDocument = getDocument(thisObj);
213 thisAsDocument.write(concatArgsAsString(args) + "\n");
214 }
215
216
217
218
219
220
221 private static HTMLDocument getDocument(final Scriptable thisObj) {
222
223
224
225
226 if (thisObj instanceof HTMLDocument document && thisObj.getPrototype() instanceof HTMLDocument) {
227 return document;
228 }
229 if (thisObj instanceof DocumentProxy proxy && thisObj.getPrototype() instanceof HTMLDocument) {
230 return (HTMLDocument) proxy.getDelegee();
231 }
232
233 throw JavaScriptEngine.reportRuntimeError("Function can't be used detached from document");
234 }
235
236
237
238
239
240
241
242 public void setExecutingDynamicExternalPosponed(final boolean executing) {
243 executionExternalPostponed_ = executing;
244 }
245
246
247
248
249
250
251
252
253
254
255 protected void write(final String content) {
256
257 if (executionExternalPostponed_) {
258 if (LOG.isDebugEnabled()) {
259 LOG.debug("skipping write for external posponed: " + content);
260 }
261 return;
262 }
263
264 if (LOG.isDebugEnabled()) {
265 LOG.debug("write: " + content);
266 }
267
268 final HtmlPage page = (HtmlPage) getDomNodeOrDie();
269 if (!page.isBeingParsed()) {
270 writeInCurrentDocument_ = false;
271 }
272
273
274 writeBuilder_.append(content);
275
276
277 if (!writeInCurrentDocument_) {
278 LOG.debug("wrote content to buffer");
279 scheduleImplicitClose();
280 return;
281 }
282 final String bufferedContent = writeBuilder_.toString();
283 if (!canAlreadyBeParsed(bufferedContent)) {
284 LOG.debug("write: not enough content to parse it now");
285 return;
286 }
287
288 writeBuilder_.setLength(0);
289 page.writeInParsedStream(bufferedContent);
290 }
291
292 private void scheduleImplicitClose() {
293 if (!closePostponedAction_) {
294 closePostponedAction_ = true;
295 final HtmlPage page = (HtmlPage) getDomNodeOrDie();
296 final WebWindow enclosingWindow = page.getEnclosingWindow();
297 page.getWebClient().getJavaScriptEngine().addPostponedAction(
298 new PostponedAction(page, "HTMLDocument.scheduleImplicitClose") {
299 @Override
300 public void execute() throws Exception {
301 if (writeBuilder_.length() != 0) {
302 close();
303 }
304 closePostponedAction_ = false;
305 }
306
307 @Override
308 public boolean isStillAlive() {
309 return !enclosingWindow.isClosed();
310 }
311 });
312 }
313 }
314
315
316
317
318
319
320
321 static boolean canAlreadyBeParsed(final String content) {
322
323
324 ParsingStatus tagState = ParsingStatus.OUTSIDE;
325 int tagNameBeginIndex = 0;
326 int scriptTagCount = 0;
327 boolean tagIsOpen = true;
328 char stringBoundary = 0;
329 boolean stringSkipNextChar = false;
330 int index = 0;
331 char openingQuote = 0;
332 for (final char currentChar : content.toCharArray()) {
333 switch (tagState) {
334 case OUTSIDE:
335 if (currentChar == '<') {
336 tagState = ParsingStatus.START;
337 tagIsOpen = true;
338 }
339 else if (scriptTagCount > 0 && (currentChar == '\'' || currentChar == '"')) {
340 tagState = ParsingStatus.IN_STRING;
341 stringBoundary = currentChar;
342 stringSkipNextChar = false;
343 }
344 break;
345 case START:
346 if (currentChar == '/') {
347 tagIsOpen = false;
348 tagNameBeginIndex = index + 1;
349 }
350 else {
351 tagNameBeginIndex = index;
352 }
353 tagState = ParsingStatus.IN_NAME;
354 break;
355 case IN_NAME:
356 if (Character.isWhitespace(currentChar) || currentChar == '>') {
357 final String tagName = content.substring(tagNameBeginIndex, index);
358 if ("script".equalsIgnoreCase(tagName)) {
359 if (tagIsOpen) {
360 scriptTagCount++;
361 }
362 else if (scriptTagCount > 0) {
363
364 scriptTagCount--;
365 }
366 }
367 if (currentChar == '>') {
368 tagState = ParsingStatus.OUTSIDE;
369 }
370 else {
371 tagState = ParsingStatus.INSIDE;
372 }
373 }
374 else if (!Character.isLetter(currentChar)) {
375 tagState = ParsingStatus.OUTSIDE;
376 }
377 break;
378 case INSIDE:
379 if (currentChar == openingQuote) {
380 openingQuote = 0;
381 }
382 else if (openingQuote == 0) {
383 if (currentChar == '\'' || currentChar == '"') {
384 openingQuote = currentChar;
385 }
386 else if (currentChar == '>' && openingQuote == 0) {
387 tagState = ParsingStatus.OUTSIDE;
388 }
389 }
390 break;
391 case IN_STRING:
392 if (stringSkipNextChar) {
393 stringSkipNextChar = false;
394 }
395 else {
396 if (currentChar == stringBoundary) {
397 tagState = ParsingStatus.OUTSIDE;
398 }
399 else if (currentChar == '\\') {
400 stringSkipNextChar = true;
401 }
402 }
403 break;
404 default:
405
406 }
407 index++;
408 }
409 if (scriptTagCount > 0 || tagState != ParsingStatus.OUTSIDE) {
410 if (LOG.isDebugEnabled()) {
411 final StringBuilder message = new StringBuilder()
412 .append("canAlreadyBeParsed() returns false for content: '")
413 .append(StringUtils.abbreviateMiddle(content, ".", 100))
414 .append("' (scriptTagCount: ")
415 .append(scriptTagCount)
416 .append(" tagState: ")
417 .append(tagState)
418 .append(')');
419 LOG.debug(message.toString());
420 }
421 return false;
422 }
423
424 return true;
425 }
426
427
428
429
430
431
432 HtmlElement getLastHtmlElement(final HtmlElement node) {
433 final DomNode lastChild = node.getLastChild();
434 if (!(lastChild instanceof HtmlElement)
435 || lastChild instanceof HtmlScript) {
436 return node;
437 }
438
439 return getLastHtmlElement((HtmlElement) lastChild);
440 }
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457 @JsxFunction
458 public HTMLDocument open(final Object url, final Object name, final Object features,
459 final Object replace) {
460
461
462 final HtmlPage page = getPage();
463 if (page.isBeingParsed()) {
464 LOG.warn("Ignoring call to open() during the parsing stage.");
465 return null;
466 }
467
468
469 if (!writeInCurrentDocument_) {
470 LOG.warn("Function open() called when document is already open.");
471 }
472 writeInCurrentDocument_ = false;
473 final WebWindow ww = getWindow().getWebWindow();
474 if (ww instanceof FrameWindow window
475 && UrlUtils.ABOUT_BLANK.equals(getPage().getUrl().toExternalForm())) {
476 final URL enclosingUrl = window.getEnclosingPage().getUrl();
477 getPage().getWebResponse().getWebRequest().setUrl(enclosingUrl);
478 }
479 return this;
480 }
481
482
483
484
485 @Override
486 @JsxFunction({FF, FF_ESR})
487 public void close() throws IOException {
488 if (writeInCurrentDocument_) {
489 LOG.warn("close() called when document is not open.");
490 }
491 else {
492 final HtmlPage page = getPage();
493 final URL url = page.getUrl();
494 final StringWebResponse webResponse = new StringWebResponse(writeBuilder_.toString(), url);
495 webResponse.setFromJavascript(true);
496 writeInCurrentDocument_ = true;
497 writeBuilder_.setLength(0);
498
499 final WebClient webClient = page.getWebClient();
500 final WebWindow window = page.getEnclosingWindow();
501
502 if (window instanceof FrameWindow frameWindow) {
503 final BaseFrameElement frame = frameWindow.getFrameElement();
504 final HtmlUnitScriptable scriptable = frame.getScriptableObject();
505 if (scriptable instanceof HTMLIFrameElement element) {
506 element.onRefresh();
507 }
508 }
509 webClient.loadWebResponseInto(webResponse, window);
510 }
511 }
512
513
514
515
516 @JsxGetter
517 @Override
518 public Element getDocumentElement() {
519 implicitCloseIfNecessary();
520 return super.getDocumentElement();
521 }
522
523
524
525
526 private void implicitCloseIfNecessary() {
527 if (!writeInCurrentDocument_) {
528 try {
529 close();
530 }
531 catch (final IOException e) {
532 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
533 }
534 }
535 }
536
537
538
539
540 @Override
541 public Node appendChild(final Object childObject) {
542 if (childObject instanceof Node childNode
543 && !isInsertableAtEnd(getPage(), childNode.getDomNodeOrDie())) {
544 throw JavaScriptEngine.asJavaScriptException(
545 getWindow(),
546 "Node cannot be inserted at the specified point in the hierarchy.",
547 org.htmlunit.javascript.host.dom.DOMException.HIERARCHY_REQUEST_ERR);
548 }
549 return super.appendChild(childObject);
550 }
551
552
553
554
555
556
557 private static boolean isInsertableAtEnd(final HtmlPage page, final DomNode node) {
558 if (node instanceof DomDocumentFragment fragment) {
559 int elements = 0;
560 for (final DomNode child : fragment.getChildren()) {
561 if (child instanceof org.w3c.dom.Element) {
562 elements++;
563 }
564 else if (child instanceof org.w3c.dom.Text) {
565 return false;
566 }
567 }
568 return elements == 0 || (elements == 1 && page.getDocumentElement() == null);
569 }
570 if (node instanceof org.w3c.dom.Element) {
571 return page.getDocumentElement() == null;
572 }
573 if (node instanceof org.w3c.dom.DocumentType) {
574 return page.getDoctype() == null && page.getDocumentElement() == null;
575 }
576 return node instanceof org.w3c.dom.Comment || node instanceof org.w3c.dom.ProcessingInstruction;
577 }
578
579
580
581
582
583
584 @JsxFunction
585 @Override
586 public HtmlUnitScriptable getElementById(final String id) {
587 implicitCloseIfNecessary();
588 final DomElement domElement = getPage().getElementById(id);
589 if (domElement == null) {
590
591 if (LOG.isDebugEnabled()) {
592 LOG.debug("getElementById(" + id + "): no DOM node found with this id");
593 }
594 return null;
595 }
596
597 final HtmlUnitScriptable jsElement = getScriptableFor(domElement);
598 if (jsElement == NOT_FOUND) {
599 if (LOG.isDebugEnabled()) {
600 LOG.debug("getElementById(" + id
601 + ") cannot return a result as there isn't a JavaScript object for the HTML element "
602 + domElement.getClass().getName());
603 }
604 return null;
605 }
606 return jsElement;
607 }
608
609
610
611
612 @Override
613 public HTMLCollection getElementsByClassName(final String className) {
614 return getDocumentElement().getElementsByClassName(className);
615 }
616
617
618
619
620 @Override
621 public NodeList getElementsByName(final String elementName) {
622 implicitCloseIfNecessary();
623
624 if ("null".equals(elementName)
625 || (elementName.isEmpty()
626 && getBrowserVersion().hasFeature(HTMLDOCUMENT_ELEMENTS_BY_NAME_EMPTY))) {
627 return NodeList.staticNodeList(getParentScope(), new ArrayList<>());
628 }
629
630 final HtmlPage page = getPage();
631 final NodeList elements = new NodeList(page, true);
632 elements.setElementsSupplier(
633 (Supplier<List<DomNode>> & Serializable)
634 () -> new ArrayList<>(page.getElementsByName(elementName)));
635
636 elements.setEffectOnCacheFunction(
637 (java.util.function.Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable)
638 event -> {
639 if ("name".equals(event.getName())) {
640 return EffectOnCache.RESET;
641 }
642 return EffectOnCache.NONE;
643 });
644
645 return elements;
646 }
647
648
649
650
651
652
653
654 @Override
655 protected Object getWithPreemption(final String name) {
656 final HtmlPage page = (HtmlPage) getDomNodeOrNull();
657 if (page == null) {
658 final Object response = getPrototype().get(name, this);
659 if (response != NOT_FOUND) {
660 return response;
661 }
662 }
663 return getIt(name);
664 }
665
666 private Object getIt(final String name) {
667 final HtmlPage page = (HtmlPage) getDomNodeOrNull();
668 if (page == null) {
669 return NOT_FOUND;
670 }
671
672
673
674
675 final List<DomNode> matchingElements = getItComputeElements(page, name);
676 final int size = matchingElements.size();
677 if (size == 0) {
678 return NOT_FOUND;
679 }
680 if (size == 1) {
681 final DomNode object = matchingElements.get(0);
682 if (object instanceof BaseFrameElement element) {
683 return element.getEnclosedWindow().getScriptableObject();
684 }
685 return super.getScriptableFor(object);
686 }
687
688 final HTMLCollection coll = new HTMLCollection(page, matchingElements) {
689 @Override
690 protected HtmlUnitScriptable getScriptableFor(final Object object) {
691 if (object instanceof BaseFrameElement element) {
692 return element.getEnclosedWindow().getScriptableObject();
693 }
694 return super.getScriptableFor(object);
695 }
696 };
697
698 coll.setElementsSupplier(
699 (Supplier<List<DomNode>> & Serializable)
700 () -> getItComputeElements(page, name));
701
702 coll.setEffectOnCacheFunction(
703 (java.util.function.Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable)
704 event -> {
705 final String attributeName = event.getName();
706 if (DomElement.NAME_ATTRIBUTE.equals(attributeName)) {
707 return EffectOnCache.RESET;
708 }
709
710 return EffectOnCache.NONE;
711 });
712
713 return coll;
714 }
715
716 static List<DomNode> getItComputeElements(final HtmlPage page, final String name) {
717 final List<DomElement> elements = page.getElementsByName(name);
718 final List<DomNode> matchingElements = new ArrayList<>();
719 for (final DomElement elt : elements) {
720 if (elt instanceof HtmlForm || elt instanceof HtmlImage || elt instanceof BaseFrameElement) {
721 matchingElements.add(elt);
722 }
723 }
724 return matchingElements;
725 }
726
727
728
729
730 @Override
731 public HTMLElement getHead() {
732 final HtmlElement head = getPage().getHead();
733 if (head == null) {
734 return null;
735 }
736 return head.getScriptableObject();
737 }
738
739
740
741
742 @Override
743 public String getTitle() {
744 return getPage().getDocumentTitle();
745 }
746
747
748
749
750 @Override
751 public void setTitle(final String title) {
752 getPage().setTitleText(title);
753 }
754
755
756
757
758 @Override
759 public HTMLElement getActiveElement() {
760 final HtmlElement activeElement = getPage().getActiveElement();
761 if (activeElement != null) {
762 return activeElement.getScriptableObject();
763 }
764 return null;
765 }
766
767
768
769
770 @Override
771 public boolean hasFocus() {
772 return getPage().getFocusedElement() != null;
773 }
774
775
776
777
778
779
780
781
782
783
784 @Override
785 @JsxFunction
786 public boolean dispatchEvent(final Event event) {
787 event.setTarget(this);
788 final ScriptResult result = fireEvent(event);
789 return !event.isAborted(result);
790 }
791
792
793
794
795 @Override
796 public Selection getSelection() {
797 return getWindow().getSelectionImpl();
798 }
799
800
801
802
803
804
805
806 @Override
807 public Attr createAttribute(final String attributeName) {
808 String name = attributeName;
809 if (!org.htmlunit.util.StringUtils.isEmptyOrNull(name)) {
810 name = org.htmlunit.util.StringUtils.toRootLowerCase(name);
811 }
812
813 return super.createAttribute(name);
814 }
815
816
817
818
819 @Override
820 public String getBaseURI() {
821 return getPage().getBaseURL().toString();
822 }
823
824
825
826
827 @Override
828 public HtmlUnitScriptable elementFromPoint(final int x, final int y) {
829 final HtmlElement element = getPage().getElementFromPoint(x, y);
830 return element == null ? null : element.getScriptableObject();
831 }
832 }