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