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
256 protected void write(final String content) {
257
258 if (executionExternalPostponed_) {
259 if (LOG.isDebugEnabled()) {
260 LOG.debug("skipping write for external posponed: " + content);
261 }
262 return;
263 }
264
265 if (LOG.isDebugEnabled()) {
266 LOG.debug("write: " + content);
267 }
268
269 final HtmlPage page = (HtmlPage) getDomNodeOrDie();
270 if (!page.isBeingParsed()) {
271 writeInCurrentDocument_ = false;
272 }
273
274
275 writeBuilder_.append(content);
276
277
278 if (!writeInCurrentDocument_) {
279 LOG.debug("wrote content to buffer");
280 scheduleImplicitClose();
281 return;
282 }
283 final String bufferedContent = writeBuilder_.toString();
284 if (!canAlreadyBeParsed(bufferedContent)) {
285 LOG.debug("write: not enough content to parse it now");
286 return;
287 }
288
289 writeBuilder_.setLength(0);
290 page.writeInParsedStream(bufferedContent);
291 }
292
293 private void scheduleImplicitClose() {
294 if (!closePostponedAction_) {
295 closePostponedAction_ = true;
296 final HtmlPage page = (HtmlPage) getDomNodeOrDie();
297 final WebWindow enclosingWindow = page.getEnclosingWindow();
298 page.getWebClient().getJavaScriptEngine().addPostponedAction(
299 new PostponedAction(page, "HTMLDocument.scheduleImplicitClose") {
300 @Override
301 public void execute() throws Exception {
302 if (writeBuilder_.length() != 0) {
303 close();
304 }
305 closePostponedAction_ = false;
306 }
307
308 @Override
309 public boolean isStillAlive() {
310 return !enclosingWindow.isClosed();
311 }
312 });
313 }
314 }
315
316
317
318
319
320
321
322 static boolean canAlreadyBeParsed(final String content) {
323
324
325 ParsingStatus tagState = ParsingStatus.OUTSIDE;
326 int tagNameBeginIndex = 0;
327 int scriptTagCount = 0;
328 boolean tagIsOpen = true;
329 char stringBoundary = 0;
330 boolean stringSkipNextChar = false;
331 int index = 0;
332 char openingQuote = 0;
333 for (final char currentChar : content.toCharArray()) {
334 switch (tagState) {
335 case OUTSIDE:
336 if (currentChar == '<') {
337 tagState = ParsingStatus.START;
338 tagIsOpen = true;
339 }
340 else if (scriptTagCount > 0 && (currentChar == '\'' || currentChar == '"')) {
341 tagState = ParsingStatus.IN_STRING;
342 stringBoundary = currentChar;
343 stringSkipNextChar = false;
344 }
345 break;
346 case START:
347 if (currentChar == '/') {
348 tagIsOpen = false;
349 tagNameBeginIndex = index + 1;
350 }
351 else {
352 tagNameBeginIndex = index;
353 }
354 tagState = ParsingStatus.IN_NAME;
355 break;
356 case IN_NAME:
357 if (Character.isWhitespace(currentChar) || currentChar == '>') {
358 final String tagName = content.substring(tagNameBeginIndex, index);
359 if ("script".equalsIgnoreCase(tagName)) {
360 if (tagIsOpen) {
361 scriptTagCount++;
362 }
363 else if (scriptTagCount > 0) {
364
365 scriptTagCount--;
366 }
367 }
368 if (currentChar == '>') {
369 tagState = ParsingStatus.OUTSIDE;
370 }
371 else {
372 tagState = ParsingStatus.INSIDE;
373 }
374 }
375 else if (!Character.isLetter(currentChar)) {
376 tagState = ParsingStatus.OUTSIDE;
377 }
378 break;
379 case INSIDE:
380 if (currentChar == openingQuote) {
381 openingQuote = 0;
382 }
383 else if (openingQuote == 0) {
384 if (currentChar == '\'' || currentChar == '"') {
385 openingQuote = currentChar;
386 }
387 else if (currentChar == '>' && openingQuote == 0) {
388 tagState = ParsingStatus.OUTSIDE;
389 }
390 }
391 break;
392 case IN_STRING:
393 if (stringSkipNextChar) {
394 stringSkipNextChar = false;
395 }
396 else {
397 if (currentChar == stringBoundary) {
398 tagState = ParsingStatus.OUTSIDE;
399 }
400 else if (currentChar == '\\') {
401 stringSkipNextChar = true;
402 }
403 }
404 break;
405 default:
406
407 }
408 index++;
409 }
410 if (scriptTagCount > 0 || tagState != ParsingStatus.OUTSIDE) {
411 if (LOG.isDebugEnabled()) {
412 final StringBuilder message = new StringBuilder()
413 .append("canAlreadyBeParsed() returns false for content: '")
414 .append(StringUtils.abbreviateMiddle(content, ".", 100))
415 .append("' (scriptTagCount: ")
416 .append(scriptTagCount)
417 .append(" tagState: ")
418 .append(tagState)
419 .append(')');
420 LOG.debug(message.toString());
421 }
422 return false;
423 }
424
425 return true;
426 }
427
428
429
430
431
432
433 HtmlElement getLastHtmlElement(final HtmlElement node) {
434 final DomNode lastChild = node.getLastChild();
435 if (!(lastChild instanceof HtmlElement)
436 || lastChild instanceof HtmlScript) {
437 return node;
438 }
439
440 return getLastHtmlElement((HtmlElement) lastChild);
441 }
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458 @JsxFunction
459 public HTMLDocument open(final Object url, final Object name, final Object features,
460 final Object replace) {
461
462
463 final HtmlPage page = getPage();
464 if (page.isBeingParsed()) {
465 LOG.warn("Ignoring call to open() during the parsing stage.");
466 return null;
467 }
468
469
470 if (!writeInCurrentDocument_) {
471 LOG.warn("Function open() called when document is already open.");
472 }
473 writeInCurrentDocument_ = false;
474 final WebWindow ww = getWindow().getWebWindow();
475 if (ww instanceof FrameWindow window
476 && UrlUtils.ABOUT_BLANK.equals(getPage().getUrl().toExternalForm())) {
477 final URL enclosingUrl = window.getEnclosingPage().getUrl();
478 getPage().getWebResponse().getWebRequest().setUrl(enclosingUrl);
479 }
480 return this;
481 }
482
483
484
485
486 @Override
487 @JsxFunction({FF, FF_ESR})
488 public void close() throws IOException {
489 if (writeInCurrentDocument_) {
490 LOG.warn("close() called when document is not open.");
491 }
492 else {
493 final HtmlPage page = getPage();
494 final URL url = page.getUrl();
495 final StringWebResponse webResponse = new StringWebResponse(writeBuilder_.toString(), url);
496 webResponse.setFromJavascript(true);
497 writeInCurrentDocument_ = true;
498 writeBuilder_.setLength(0);
499
500 final WebClient webClient = page.getWebClient();
501 final WebWindow window = page.getEnclosingWindow();
502
503 if (window instanceof FrameWindow frameWindow) {
504 final BaseFrameElement frame = frameWindow.getFrameElement();
505 final HtmlUnitScriptable scriptable = frame.getScriptableObject();
506 if (scriptable instanceof HTMLIFrameElement element) {
507 element.onRefresh();
508 }
509 }
510 webClient.loadWebResponseInto(webResponse, window);
511 }
512 }
513
514
515
516
517 @JsxGetter
518 @Override
519 public Element getDocumentElement() {
520 implicitCloseIfNecessary();
521 return super.getDocumentElement();
522 }
523
524
525
526
527 private void implicitCloseIfNecessary() {
528 if (!writeInCurrentDocument_) {
529 try {
530 close();
531 }
532 catch (final IOException e) {
533 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
534 }
535 }
536 }
537
538
539
540
541 @Override
542 public Node appendChild(final Object childObject) {
543 throw JavaScriptEngine.asJavaScriptException(
544 getWindow(),
545 "Node cannot be inserted at the specified point in the hierarchy.",
546 org.htmlunit.javascript.host.dom.DOMException.HIERARCHY_REQUEST_ERR);
547 }
548
549
550
551
552
553
554 @JsxFunction
555 @Override
556 public HtmlUnitScriptable getElementById(final String id) {
557 implicitCloseIfNecessary();
558 final DomElement domElement = getPage().getElementById(id);
559 if (null == domElement) {
560
561 if (LOG.isDebugEnabled()) {
562 LOG.debug("getElementById(" + id + "): no DOM node found with this id");
563 }
564 return null;
565 }
566
567 final HtmlUnitScriptable jsElement = getScriptableFor(domElement);
568 if (jsElement == NOT_FOUND) {
569 if (LOG.isDebugEnabled()) {
570 LOG.debug("getElementById(" + id
571 + ") cannot return a result as there isn't a JavaScript object for the HTML element "
572 + domElement.getClass().getName());
573 }
574 return null;
575 }
576 return jsElement;
577 }
578
579
580
581
582 @Override
583 public HTMLCollection getElementsByClassName(final String className) {
584 return getDocumentElement().getElementsByClassName(className);
585 }
586
587
588
589
590 @Override
591 public NodeList getElementsByName(final String elementName) {
592 implicitCloseIfNecessary();
593
594 if ("null".equals(elementName)
595 || (elementName.isEmpty()
596 && getBrowserVersion().hasFeature(HTMLDOCUMENT_ELEMENTS_BY_NAME_EMPTY))) {
597 return NodeList.staticNodeList(getParentScope(), new ArrayList<>());
598 }
599
600 final HtmlPage page = getPage();
601 final NodeList elements = new NodeList(page, true);
602 elements.setElementsSupplier(
603 (Supplier<List<DomNode>> & Serializable)
604 () -> new ArrayList<>(page.getElementsByName(elementName)));
605
606 elements.setEffectOnCacheFunction(
607 (java.util.function.Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable)
608 event -> {
609 if ("name".equals(event.getName())) {
610 return EffectOnCache.RESET;
611 }
612 return EffectOnCache.NONE;
613 });
614
615 return elements;
616 }
617
618
619
620
621
622
623
624 @Override
625 protected Object getWithPreemption(final String name) {
626 final HtmlPage page = (HtmlPage) getDomNodeOrNull();
627 if (page == null) {
628 final Object response = getPrototype().get(name, this);
629 if (response != NOT_FOUND) {
630 return response;
631 }
632 }
633 return getIt(name);
634 }
635
636 private Object getIt(final String name) {
637 final HtmlPage page = (HtmlPage) getDomNodeOrNull();
638 if (page == null) {
639 return NOT_FOUND;
640 }
641
642
643
644
645 final List<DomNode> matchingElements = getItComputeElements(page, name);
646 final int size = matchingElements.size();
647 if (size == 0) {
648 return NOT_FOUND;
649 }
650 if (size == 1) {
651 final DomNode object = matchingElements.get(0);
652 if (object instanceof BaseFrameElement element) {
653 return element.getEnclosedWindow().getScriptableObject();
654 }
655 return super.getScriptableFor(object);
656 }
657
658 final HTMLCollection coll = new HTMLCollection(page, matchingElements) {
659 @Override
660 protected HtmlUnitScriptable getScriptableFor(final Object object) {
661 if (object instanceof BaseFrameElement element) {
662 return element.getEnclosedWindow().getScriptableObject();
663 }
664 return super.getScriptableFor(object);
665 }
666 };
667
668 coll.setElementsSupplier(
669 (Supplier<List<DomNode>> & Serializable)
670 () -> getItComputeElements(page, name));
671
672 coll.setEffectOnCacheFunction(
673 (java.util.function.Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable)
674 event -> {
675 final String attributeName = event.getName();
676 if (DomElement.NAME_ATTRIBUTE.equals(attributeName)) {
677 return EffectOnCache.RESET;
678 }
679
680 return EffectOnCache.NONE;
681 });
682
683 return coll;
684 }
685
686 static List<DomNode> getItComputeElements(final HtmlPage page, final String name) {
687 final List<DomElement> elements = page.getElementsByName(name);
688 final List<DomNode> matchingElements = new ArrayList<>();
689 for (final DomElement elt : elements) {
690 if (elt instanceof HtmlForm || elt instanceof HtmlImage || elt instanceof BaseFrameElement) {
691 matchingElements.add(elt);
692 }
693 }
694 return matchingElements;
695 }
696
697
698
699
700 @Override
701 public HTMLElement getHead() {
702 final HtmlElement head = getPage().getHead();
703 if (head == null) {
704 return null;
705 }
706 return head.getScriptableObject();
707 }
708
709
710
711
712 @Override
713 public String getTitle() {
714 return getPage().getTitleText();
715 }
716
717
718
719
720 @Override
721 public void setTitle(final String title) {
722 getPage().setTitleText(title);
723 }
724
725
726
727
728 @Override
729 public HTMLElement getActiveElement() {
730 final HtmlElement activeElement = getPage().getActiveElement();
731 if (activeElement != null) {
732 return activeElement.getScriptableObject();
733 }
734 return null;
735 }
736
737
738
739
740 @Override
741 public boolean hasFocus() {
742 return getPage().getFocusedElement() != null;
743 }
744
745
746
747
748
749
750
751
752
753
754 @Override
755 @JsxFunction
756 public boolean dispatchEvent(final Event event) {
757 event.setTarget(this);
758 final ScriptResult result = fireEvent(event);
759 return !event.isAborted(result);
760 }
761
762
763
764
765 @Override
766 public Selection getSelection() {
767 return getWindow().getSelectionImpl();
768 }
769
770
771
772
773
774
775
776 @Override
777 public Attr createAttribute(final String attributeName) {
778 String name = attributeName;
779 if (!org.htmlunit.util.StringUtils.isEmptyOrNull(name)) {
780 name = org.htmlunit.util.StringUtils.toRootLowerCase(name);
781 }
782
783 return super.createAttribute(name);
784 }
785
786
787
788
789 @Override
790 public String getBaseURI() {
791 return getPage().getBaseURL().toString();
792 }
793
794
795
796
797 @Override
798 public HtmlUnitScriptable elementFromPoint(final int x, final int y) {
799 final HtmlElement element = getPage().getElementFromPoint(x, y);
800 return element == null ? null : element.getScriptableObject();
801 }
802 }