View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
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   * A JavaScript object for {@code HTMLDocument}.
69   *
70   * @author Mike Bowler
71   * @author David K. Taylor
72   * @author Chen Jun
73   * @author Christian Sell
74   * @author Chris Erskine
75   * @author Marc Guillemot
76   * @author Daniel Gredler
77   * @author Michael Ottati
78   * @author George Murnock
79   * @author Ahmed Ashour
80   * @author Rob Di Marco
81   * @author Sudhan Moghe
82   * @author Mike Dirolf
83   * @author Ronald Brill
84   * @author Frank Danek
85   * @author Sven Strickroth
86   *
87   * @see <a href="http://msdn.microsoft.com/en-us/library/ms535862.aspx">MSDN documentation</a>
88   * @see <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-html.html#ID-7068919">
89   *     W3C DOM Level 1</a>
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      /** The buffer that will be used for calls to document.write(). */
99      private final StringBuilder writeBuilder_ = new StringBuilder();
100     private boolean writeInCurrentDocument_ = true;
101 
102     private boolean closePostponedAction_;
103     private boolean executionExternalPostponed_;
104 
105     /**
106      * JavaScript constructor.
107      */
108     @Override
109     @JsxConstructor
110     public void jsConstructor() {
111         super.jsConstructor();
112     }
113 
114     /**
115      * {@inheritDoc}
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      * Returns the HTML page that this document is modeling.
129      * @return the HTML page that this document is modeling
130      */
131     @Override
132     public HtmlPage getPage() {
133         return (HtmlPage) getDomNodeOrDie();
134     }
135 
136     /**
137      * JavaScript function "write" may accept a variable number of arguments.
138      * @param context the JavaScript context
139      * @param scope the scope
140      * @param thisObj the scriptable
141      * @param args the arguments passed into the method
142      * @param function the function
143      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536782.aspx">MSDN documentation</a>
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      * Converts the arguments to strings and concatenate them.
154      * @param args the JavaScript arguments
155      * @return the string concatenation
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      * Moves a given Node inside the invoking node as a direct child, before a given reference node.
167      *
168      * @param context the JavaScript context
169      * @param scope the scope
170      * @param thisObj the scriptable
171      * @param args the arguments passed into the method
172      * @param function the function
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      * JavaScript function "writeln" may accept a variable number of arguments.
182      * @param context the JavaScript context
183      * @param scope the scope
184      * @param thisObj the scriptable
185      * @param args the arguments passed into the method
186      * @param function the function
187      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536783.aspx">MSDN documentation</a>
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      * Returns the current document instance, using <code>thisObj</code> as a hint.
198      * @param thisObj a hint as to the current document (maybe the prototype when function is used without "this")
199      * @return the current document instance
200      */
201     private static HTMLDocument getDocument(final Scriptable thisObj) {
202         // if function is used "detached", then thisObj is the top scope (ie Window), not the real object
203         // cf unit test DocumentTest#testDocumentWrite_AssignedToVar
204         // may be the prototype too
205         // cf DocumentTest#testDocumentWrite_AssignedToVar2
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      * This a hack!!! A cleaner way is welcome.
218      * Handle a case where document.write() is simply ignored.
219      * See HTMLDocumentWrite2Test.write_fromScriptAddedWithAppendChild_external.
220      * @param executing indicates if executing or not
221      */
222     public void setExecutingDynamicExternalPosponed(final boolean executing) {
223         executionExternalPostponed_ = executing;
224     }
225 
226     /**
227      * JavaScript function "write".
228      * <p>
229      * See http://www.whatwg.org/specs/web-apps/current-work/multipage/section-dynamic.html for
230      * a good description of the semantics of open(), write(), writeln() and close().
231      *
232      * @param content the content to write
233      */
234     protected void write(final String content) {
235         // really strange: if called from an external script loaded as postponed action, write is ignored!!!
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         // Add content to the content buffer.
253         writeBuilder_.append(content);
254 
255         // If open() was called; don't write to doc yet -- wait for call to close().
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      * Indicates if the content is a well formed HTML snippet that can already be parsed to be added to the DOM.
296      *
297      * @param content the HTML snippet
298      * @return {@code false} if it not well formed
299      */
300     static boolean canAlreadyBeParsed(final String content) {
301         // all <script> must have their </script> because the parser doesn't close automatically this tag
302         // All tags must be complete, that is from '<' to '>'.
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                                 // Ignore extra close tags for now. Let the parser deal with them.
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                     // nothing
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      * Gets the node that is the last one when exploring following nodes, depth-first.
408      * @param node the node to search
409      * @return the searched node
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      * JavaScript function "open".
423      * <p>
424      * See http://www.whatwg.org/specs/web-apps/current-work/multipage/section-dynamic.html for
425      * a good description of the semantics of open(), write(), writeln() and close().
426      *
427      * @param url when a new document is opened, <i>url</i> is a String that specifies a MIME type for the document.
428      *        When a new window is opened, <i>url</i> is a String that specifies the URL to render in the new window
429      * @param name the name
430      * @param features the features
431      * @param replace whether to replace in the history list or no
432      * @return a reference to the new document object.
433      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536652.aspx">MSDN documentation</a>
434      */
435     @JsxFunction
436     public HTMLDocument open(final Object url, final Object name, final Object features,
437             final Object replace) {
438         // Any open() invocations are ignored during the parsing stage, because write() and
439         // writeln() invocations will directly append content to the current insertion point.
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         // We're not in the parsing stage; OK to continue.
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      * {@inheritDoc}
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             // reset isAttachedToPageDuringOnload_ to trigger the onload event for chrome also
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      * {@inheritDoc}
493      */
494     @JsxGetter
495     @Override
496     public Element getDocumentElement() {
497         implicitCloseIfNecessary();
498         return super.getDocumentElement();
499     }
500 
501     /**
502      * Closes the document implicitly, i.e. flushes the <code>document.write</code> buffer (IE only).
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      * {@inheritDoc}
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      * Returns the element with the specified ID, or {@code null} if that element could not be found.
528      * @param id the ID to search for
529      * @return the element, or {@code null} if it could not be found
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             // Just fall through - result is already set to null
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      * {@inheritDoc}
558      */
559     @Override
560     public HTMLCollection getElementsByClassName(final String className) {
561         return getDocumentElement().getElementsByClassName(className);
562     }
563 
564     /**
565      * {@inheritDoc}
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      * Calls to <code>document.XYZ</code> should first look at elements named <code>XYZ</code> before
597      * using standard functions.
598      * <p>
599      * {@inheritDoc}
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         // for performance,
620         // we will calculate the elements to decide if we really have
621         // to really create a HTMLCollection or not
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      * {@inheritDoc}
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      * {@inheritDoc}
688      */
689     @Override
690     public String getTitle() {
691         return getPage().getTitleText();
692     }
693 
694     /**
695      * {@inheritDoc}
696      */
697     @Override
698     public void setTitle(final String title) {
699         getPage().setTitleText(title);
700     }
701 
702     /**
703      * {@inheritDoc}
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      * {@inheritDoc}
716      */
717     @Override
718     public boolean hasFocus() {
719         return getPage().getFocusedElement() != null;
720     }
721 
722     /**
723      * Dispatches an event into the event system (standards-conformant browsers only). See
724      * <a href="https://developer.mozilla.org/en-US/docs/DOM/element.dispatchEvent">the Gecko
725      * DOM reference</a> for more information.
726      *
727      * @param event the event to be dispatched
728      * @return {@code false} if at least one of the event handlers which handled the event
729      *         called <code>preventDefault</code>; {@code true} otherwise
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      * {@inheritDoc}
741      */
742     @Override
743     public Selection getSelection() {
744         return getWindow().getSelectionImpl();
745     }
746 
747     /**
748      * Creates a new HTML attribute with the specified name.
749      *
750      * @param attributeName the name of the attribute to create
751      * @return an attribute with the specified name
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      * {@inheritDoc}
765      */
766     @Override
767     public String getBaseURI() {
768         return getPage().getBaseURL().toString();
769     }
770 
771     /**
772      * {@inheritDoc}
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 }