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.CHROME;
19 import static org.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
20 import static org.htmlunit.javascript.configuration.SupportedBrowser.FF;
21 import static org.htmlunit.javascript.configuration.SupportedBrowser.FF_ESR;
22
23 import java.io.IOException;
24 import java.io.Serializable;
25 import java.net.URL;
26 import java.util.ArrayList;
27 import java.util.List;
28 import java.util.function.Supplier;
29
30 import org.apache.commons.lang3.StringUtils;
31 import org.apache.commons.logging.Log;
32 import org.apache.commons.logging.LogFactory;
33 import org.htmlunit.ScriptResult;
34 import org.htmlunit.StringWebResponse;
35 import org.htmlunit.WebClient;
36 import org.htmlunit.WebWindow;
37 import org.htmlunit.corejs.javascript.Context;
38 import org.htmlunit.corejs.javascript.Function;
39 import org.htmlunit.corejs.javascript.Scriptable;
40 import org.htmlunit.corejs.javascript.VarScope;
41 import org.htmlunit.html.BaseFrameElement;
42 import org.htmlunit.html.DomElement;
43 import org.htmlunit.html.DomNode;
44 import org.htmlunit.html.FrameWindow;
45 import org.htmlunit.html.HtmlAttributeChangeEvent;
46 import org.htmlunit.html.HtmlElement;
47 import org.htmlunit.html.HtmlForm;
48 import org.htmlunit.html.HtmlImage;
49 import org.htmlunit.html.HtmlPage;
50 import org.htmlunit.html.HtmlScript;
51 import org.htmlunit.javascript.HtmlUnitScriptable;
52 import org.htmlunit.javascript.JavaScriptEngine;
53 import org.htmlunit.javascript.PostponedAction;
54 import org.htmlunit.javascript.configuration.JsxClass;
55 import org.htmlunit.javascript.configuration.JsxConstructor;
56 import org.htmlunit.javascript.configuration.JsxFunction;
57 import org.htmlunit.javascript.configuration.JsxGetter;
58 import org.htmlunit.javascript.configuration.JsxStaticFunction;
59 import org.htmlunit.javascript.host.Element;
60 import org.htmlunit.javascript.host.dom.AbstractList.EffectOnCache;
61 import org.htmlunit.javascript.host.dom.Attr;
62 import org.htmlunit.javascript.host.dom.Document;
63 import org.htmlunit.javascript.host.dom.Node;
64 import org.htmlunit.javascript.host.dom.NodeList;
65 import org.htmlunit.javascript.host.dom.Selection;
66 import org.htmlunit.javascript.host.event.Event;
67 import org.htmlunit.util.UrlUtils;
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
93 @JsxClass
94 public class HTMLDocument extends Document {
95
96 private static final Log LOG = LogFactory.getLog(HTMLDocument.class);
97
98 private enum ParsingStatus { OUTSIDE, START, IN_NAME, INSIDE, IN_STRING }
99
100
101 private final StringBuilder writeBuilder_ = new StringBuilder();
102 private boolean writeInCurrentDocument_ = true;
103
104 private boolean closePostponedAction_;
105 private boolean executionExternalPostponed_;
106
107
108
109
110 @Override
111 @JsxConstructor
112 public void jsConstructor() {
113 super.jsConstructor();
114 }
115
116
117
118
119 @Override
120 public DomNode getDomNodeOrDie() {
121 try {
122 return super.getDomNodeOrDie();
123 }
124 catch (final IllegalStateException e) {
125 throw JavaScriptEngine.typeError("No node attached to this object");
126 }
127 }
128
129
130
131
132
133 @Override
134 public HtmlPage getPage() {
135 return (HtmlPage) getDomNodeOrDie();
136 }
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151 @JsxStaticFunction
152 public static HTMLDocument parseHTMLUnsafe(final Context cx, final VarScope scope,
153 final Scriptable thisObj, final Object[] args, final Function funObj) {
154 return (HTMLDocument) Document.parseHTMLUnsafe(cx, scope, thisObj, args, funObj);
155 }
156
157
158
159
160
161
162
163
164
165
166 @JsxFunction
167 public static void write(final Context context, final VarScope scope,
168 final Scriptable thisObj, final Object[] args, final Function function) {
169 final HTMLDocument thisAsDocument = getDocument(thisObj);
170 thisAsDocument.write(concatArgsAsString(args));
171 }
172
173
174
175
176
177
178 private static String concatArgsAsString(final Object[] args) {
179 final StringBuilder builder = new StringBuilder();
180 for (final Object arg : args) {
181 builder.append(JavaScriptEngine.toString(arg));
182 }
183 return builder.toString();
184 }
185
186
187
188
189
190
191
192
193
194
195 @JsxFunction({CHROME, EDGE, FF})
196 public static void moveBefore(final Context context, final VarScope scope,
197 final Scriptable thisObj, final Object[] args, final Function function) {
198 Node.moveBefore(context, scope, thisObj, args, function);
199 }
200
201
202
203
204
205
206
207
208
209
210 @JsxFunction
211 public static void writeln(final Context context, final VarScope scope,
212 final Scriptable thisObj, final Object[] args, final Function function) {
213 final HTMLDocument thisAsDocument = getDocument(thisObj);
214 thisAsDocument.write(concatArgsAsString(args) + "\n");
215 }
216
217
218
219
220
221
222 private static HTMLDocument getDocument(final Scriptable thisObj) {
223
224
225
226
227 if (thisObj instanceof HTMLDocument document && thisObj.getPrototype() instanceof HTMLDocument) {
228 return document;
229 }
230 if (thisObj instanceof DocumentProxy proxy && thisObj.getPrototype() instanceof HTMLDocument) {
231 return (HTMLDocument) proxy.getDelegee();
232 }
233
234 throw JavaScriptEngine.reportRuntimeError("Function can't be used detached from document");
235 }
236
237
238
239
240
241
242
243 public void setExecutingDynamicExternalPosponed(final boolean executing) {
244 executionExternalPostponed_ = executing;
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() retruns 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 @JsxFunction
457 public HTMLDocument open(final Object url, final Object name, final Object features,
458 final Object replace) {
459
460
461 final HtmlPage page = getPage();
462 if (page.isBeingParsed()) {
463 LOG.warn("Ignoring call to open() during the parsing stage.");
464 return null;
465 }
466
467
468 if (!writeInCurrentDocument_) {
469 LOG.warn("Function open() called when document is already open.");
470 }
471 writeInCurrentDocument_ = false;
472 final WebWindow ww = getWindow().getWebWindow();
473 if (ww instanceof FrameWindow window
474 && UrlUtils.ABOUT_BLANK.equals(getPage().getUrl().toExternalForm())) {
475 final URL enclosingUrl = window.getEnclosingPage().getUrl();
476 getPage().getWebResponse().getWebRequest().setUrl(enclosingUrl);
477 }
478 return this;
479 }
480
481
482
483
484 @Override
485 @JsxFunction({FF, FF_ESR})
486 public void close() throws IOException {
487 if (writeInCurrentDocument_) {
488 LOG.warn("close() called when document is not open.");
489 }
490 else {
491 final HtmlPage page = getPage();
492 final URL url = page.getUrl();
493 final StringWebResponse webResponse = new StringWebResponse(writeBuilder_.toString(), url);
494 webResponse.setFromJavascript(true);
495 writeInCurrentDocument_ = true;
496 writeBuilder_.setLength(0);
497
498 final WebClient webClient = page.getWebClient();
499 final WebWindow window = page.getEnclosingWindow();
500
501 if (window instanceof FrameWindow frameWindow) {
502 final BaseFrameElement frame = frameWindow.getFrameElement();
503 final HtmlUnitScriptable scriptable = frame.getScriptableObject();
504 if (scriptable instanceof HTMLIFrameElement element) {
505 element.onRefresh();
506 }
507 }
508 webClient.loadWebResponseInto(webResponse, window);
509 }
510 }
511
512
513
514
515 @JsxGetter
516 @Override
517 public Element getDocumentElement() {
518 implicitCloseIfNecessary();
519 return super.getDocumentElement();
520 }
521
522
523
524
525 private void implicitCloseIfNecessary() {
526 if (!writeInCurrentDocument_) {
527 try {
528 close();
529 }
530 catch (final IOException e) {
531 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
532 }
533 }
534 }
535
536
537
538
539 @Override
540 public Node appendChild(final Object childObject) {
541 throw JavaScriptEngine.asJavaScriptException(
542 getWindow(),
543 "Node cannot be inserted at the specified point in the hierarchy.",
544 org.htmlunit.javascript.host.dom.DOMException.HIERARCHY_REQUEST_ERR);
545 }
546
547
548
549
550
551
552 @JsxFunction
553 @Override
554 public HtmlUnitScriptable getElementById(final String id) {
555 implicitCloseIfNecessary();
556 final DomElement domElement = getPage().getElementById(id);
557 if (null == domElement) {
558
559 if (LOG.isDebugEnabled()) {
560 LOG.debug("getElementById(" + id + "): no DOM node found with this id");
561 }
562 return null;
563 }
564
565 final HtmlUnitScriptable jsElement = getScriptableFor(domElement);
566 if (jsElement == NOT_FOUND) {
567 if (LOG.isDebugEnabled()) {
568 LOG.debug("getElementById(" + id
569 + ") cannot return a result as there isn't a JavaScript object for the HTML element "
570 + domElement.getClass().getName());
571 }
572 return null;
573 }
574 return jsElement;
575 }
576
577
578
579
580 @Override
581 public HTMLCollection getElementsByClassName(final String className) {
582 return getDocumentElement().getElementsByClassName(className);
583 }
584
585
586
587
588 @Override
589 public NodeList getElementsByName(final String elementName) {
590 implicitCloseIfNecessary();
591
592 if ("null".equals(elementName)
593 || (elementName.isEmpty()
594 && getBrowserVersion().hasFeature(HTMLDOCUMENT_ELEMENTS_BY_NAME_EMPTY))) {
595 return NodeList.staticNodeList(getParentScope(), new ArrayList<>());
596 }
597
598 final HtmlPage page = getPage();
599 final NodeList elements = new NodeList(page, true);
600 elements.setElementsSupplier(
601 (Supplier<List<DomNode>> & Serializable)
602 () -> new ArrayList<>(page.getElementsByName(elementName)));
603
604 elements.setEffectOnCacheFunction(
605 (java.util.function.Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable)
606 event -> {
607 if ("name".equals(event.getName())) {
608 return EffectOnCache.RESET;
609 }
610 return EffectOnCache.NONE;
611 });
612
613 return elements;
614 }
615
616
617
618
619
620
621
622 @Override
623 protected Object getWithPreemption(final String name) {
624 final HtmlPage page = (HtmlPage) getDomNodeOrNull();
625 if (page == null) {
626 final Object response = getPrototype().get(name, this);
627 if (response != NOT_FOUND) {
628 return response;
629 }
630 }
631 return getIt(name);
632 }
633
634 private Object getIt(final String name) {
635 final HtmlPage page = (HtmlPage) getDomNodeOrNull();
636 if (page == null) {
637 return NOT_FOUND;
638 }
639
640
641
642
643 final List<DomNode> matchingElements = getItComputeElements(page, name);
644 final int size = matchingElements.size();
645 if (size == 0) {
646 return NOT_FOUND;
647 }
648 if (size == 1) {
649 final DomNode object = matchingElements.get(0);
650 if (object instanceof BaseFrameElement element) {
651 return element.getEnclosedWindow().getScriptableObject();
652 }
653 return super.getScriptableFor(object);
654 }
655
656 final HTMLCollection coll = new HTMLCollection(page, matchingElements) {
657 @Override
658 protected HtmlUnitScriptable getScriptableFor(final Object object) {
659 if (object instanceof BaseFrameElement element) {
660 return element.getEnclosedWindow().getScriptableObject();
661 }
662 return super.getScriptableFor(object);
663 }
664 };
665
666 coll.setElementsSupplier(
667 (Supplier<List<DomNode>> & Serializable)
668 () -> getItComputeElements(page, name));
669
670 coll.setEffectOnCacheFunction(
671 (java.util.function.Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable)
672 event -> {
673 final String attributeName = event.getName();
674 if (DomElement.NAME_ATTRIBUTE.equals(attributeName)) {
675 return EffectOnCache.RESET;
676 }
677
678 return EffectOnCache.NONE;
679 });
680
681 return coll;
682 }
683
684 static List<DomNode> getItComputeElements(final HtmlPage page, final String name) {
685 final List<DomElement> elements = page.getElementsByName(name);
686 final List<DomNode> matchingElements = new ArrayList<>();
687 for (final DomElement elt : elements) {
688 if (elt instanceof HtmlForm || elt instanceof HtmlImage || elt instanceof BaseFrameElement) {
689 matchingElements.add(elt);
690 }
691 }
692 return matchingElements;
693 }
694
695
696
697
698 @Override
699 public HTMLElement getHead() {
700 final HtmlElement head = getPage().getHead();
701 if (head == null) {
702 return null;
703 }
704 return head.getScriptableObject();
705 }
706
707
708
709
710 @Override
711 public String getTitle() {
712 return getPage().getTitleText();
713 }
714
715
716
717
718 @Override
719 public void setTitle(final String title) {
720 getPage().setTitleText(title);
721 }
722
723
724
725
726 @Override
727 public HTMLElement getActiveElement() {
728 final HtmlElement activeElement = getPage().getActiveElement();
729 if (activeElement != null) {
730 return activeElement.getScriptableObject();
731 }
732 return null;
733 }
734
735
736
737
738 @Override
739 public boolean hasFocus() {
740 return getPage().getFocusedElement() != null;
741 }
742
743
744
745
746
747
748
749
750
751
752 @Override
753 @JsxFunction
754 public boolean dispatchEvent(final Event event) {
755 event.setTarget(this);
756 final ScriptResult result = fireEvent(event);
757 return !event.isAborted(result);
758 }
759
760
761
762
763 @Override
764 public Selection getSelection() {
765 return getWindow().getSelectionImpl();
766 }
767
768
769
770
771
772
773
774 @Override
775 public Attr createAttribute(final String attributeName) {
776 String name = attributeName;
777 if (!org.htmlunit.util.StringUtils.isEmptyOrNull(name)) {
778 name = org.htmlunit.util.StringUtils.toRootLowerCase(name);
779 }
780
781 return super.createAttribute(name);
782 }
783
784
785
786
787 @Override
788 public String getBaseURI() {
789 return getPage().getBaseURL().toString();
790 }
791
792
793
794
795 @Override
796 public HtmlUnitScriptable elementFromPoint(final int x, final int y) {
797 final HtmlElement element = getPage().getElementFromPoint(x, y);
798 return element == null ? null : element.getScriptableObject();
799 }
800 }