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