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