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.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   * A JavaScript object for {@code HTMLDocument}.
68   *
69   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
70   * @author David K. Taylor
71   * @author <a href="mailto:chen_jun@users.sourceforge.net">Chen Jun</a>
72   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
73   * @author Chris Erskine
74   * @author Marc Guillemot
75   * @author Daniel Gredler
76   * @author Michael Ottati
77   * @author <a href="mailto:george@murnock.com">George Murnock</a>
78   * @author Ahmed Ashour
79   * @author Rob Di Marco
80   * @author Sudhan Moghe
81   * @author <a href="mailto:mike@10gen.com">Mike Dirolf</a>
82   * @author Ronald Brill
83   * @author Frank Danek
84   * @author Sven Strickroth
85   *
86   * @see <a href="http://msdn.microsoft.com/en-us/library/ms535862.aspx">MSDN documentation</a>
87   * @see <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-html.html#ID-7068919">
88   * W3C DOM Level 1</a>
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      /** The buffer that will be used for calls to document.write(). */
98      private final StringBuilder writeBuilder_ = new StringBuilder();
99      private boolean writeInCurrentDocument_ = true;
100 
101     private boolean closePostponedAction_;
102     private boolean executionExternalPostponed_;
103 
104     /**
105      * JavaScript constructor.
106      */
107     @Override
108     @JsxConstructor
109     public void jsConstructor() {
110         super.jsConstructor();
111     }
112 
113     /**
114      * {@inheritDoc}
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      * Returns the HTML page that this document is modeling.
128      * @return the HTML page that this document is modeling
129      */
130     @Override
131     public HtmlPage getPage() {
132         return (HtmlPage) getDomNodeOrDie();
133     }
134 
135     /**
136      * JavaScript function "write" may accept a variable number of arguments.
137      * @param context the JavaScript context
138      * @param scope the scope
139      * @param thisObj the scriptable
140      * @param args the arguments passed into the method
141      * @param function the function
142      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536782.aspx">MSDN documentation</a>
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      * Converts the arguments to strings and concatenate them.
153      * @param args the JavaScript arguments
154      * @return the string concatenation
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      * JavaScript function "writeln" may accept a variable number of arguments.
166      * @param context the JavaScript context
167      * @param scope the scope
168      * @param thisObj the scriptable
169      * @param args the arguments passed into the method
170      * @param function the function
171      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536783.aspx">MSDN documentation</a>
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      * Returns the current document instance, using <code>thisObj</code> as a hint.
182      * @param thisObj a hint as to the current document (may be the prototype when function is used without "this")
183      * @return the current document instance
184      */
185     private static HTMLDocument getDocument(final Scriptable thisObj) {
186         // if function is used "detached", then thisObj is the top scope (ie Window), not the real object
187         // cf unit test DocumentTest#testDocumentWrite_AssignedToVar
188         // may be the prototype too
189         // cf DocumentTest#testDocumentWrite_AssignedToVar2
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      * This a hack!!! A cleaner way is welcome.
202      * Handle a case where document.write is simply ignored.
203      * See HTMLDocumentWrite2Test.write_fromScriptAddedWithAppendChild_external.
204      * @param executing indicates if executing or not
205      */
206     public void setExecutingDynamicExternalPosponed(final boolean executing) {
207         executionExternalPostponed_ = executing;
208     }
209 
210     /**
211      * JavaScript function "write".
212      * <p>
213      * See http://www.whatwg.org/specs/web-apps/current-work/multipage/section-dynamic.html for
214      * a good description of the semantics of open(), write(), writeln() and close().
215      *
216      * @param content the content to write
217      */
218     protected void write(final String content) {
219         // really strange: if called from an external script loaded as postponed action, write is ignored!!!
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         // Add content to the content buffer.
237         writeBuilder_.append(content);
238 
239         // If open() was called; don't write to doc yet -- wait for call to close().
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      * Indicates if the content is a well formed HTML snippet that can already be parsed to be added to the DOM.
280      *
281      * @param content the HTML snippet
282      * @return {@code false} if it not well formed
283      */
284     static boolean canAlreadyBeParsed(final String content) {
285         // all <script> must have their </script> because the parser doesn't close automatically this tag
286         // All tags must be complete, that is from '<' to '>'.
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                                 // Ignore extra close tags for now. Let the parser deal with them.
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                     // nothing
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      * Gets the node that is the last one when exploring following nodes, depth-first.
392      * @param node the node to search
393      * @return the searched node
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      * JavaScript function "open".
407      * <p>
408      * See http://www.whatwg.org/specs/web-apps/current-work/multipage/section-dynamic.html for
409      * a good description of the semantics of open(), write(), writeln() and close().
410      *
411      * @param url when a new document is opened, <i>url</i> is a String that specifies a MIME type for the document.
412      *        When a new window is opened, <i>url</i> is a String that specifies the URL to render in the new window
413      * @param name the name
414      * @param features the features
415      * @param replace whether to replace in the history list or no
416      * @return a reference to the new document object.
417      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536652.aspx">MSDN documentation</a>
418      */
419     @JsxFunction
420     public HTMLDocument open(final Object url, final Object name, final Object features,
421             final Object replace) {
422         // Any open() invocations are ignored during the parsing stage, because write() and
423         // writeln() invocations will directly append content to the current insertion point.
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         // We're not in the parsing stage; OK to continue.
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      * {@inheritDoc}
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             // reset isAttachedToPageDuringOnload_ to trigger the onload event for chrome also
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      * {@inheritDoc}
477      */
478     @JsxGetter
479     @Override
480     public Element getDocumentElement() {
481         implicitCloseIfNecessary();
482         return super.getDocumentElement();
483     }
484 
485     /**
486      * Closes the document implicitly, i.e. flushes the <code>document.write</code> buffer (IE only).
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      * {@inheritDoc}
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      * Returns the element with the specified ID, or {@code null} if that element could not be found.
512      * @param id the ID to search for
513      * @return the element, or {@code null} if it could not be found
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             // Just fall through - result is already set to null
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      * {@inheritDoc}
542      */
543     @Override
544     public HTMLCollection getElementsByClassName(final String className) {
545         return getDocumentElement().getElementsByClassName(className);
546     }
547 
548     /**
549      * {@inheritDoc}
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      * Calls to <code>document.XYZ</code> should first look at elements named <code>XYZ</code> before
581      * using standard functions.
582      * <p>
583      * {@inheritDoc}
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         // for performance
606         // we will calculate the elements to decide if we really have
607         // to really create a HTMLCollection or not
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      * {@inheritDoc}
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      * {@inheritDoc}
676      */
677     @Override
678     public String getTitle() {
679         return getPage().getTitleText();
680     }
681 
682     /**
683      * {@inheritDoc}
684      */
685     @Override
686     public void setTitle(final String title) {
687         getPage().setTitleText(title);
688     }
689 
690     /**
691      * {@inheritDoc}
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      * {@inheritDoc}
704      */
705     @Override
706     public boolean hasFocus() {
707         return getPage().getFocusedElement() != null;
708     }
709 
710     /**
711      * Dispatches an event into the event system (standards-conformant browsers only). See
712      * <a href="https://developer.mozilla.org/en-US/docs/DOM/element.dispatchEvent">the Gecko
713      * DOM reference</a> for more information.
714      *
715      * @param event the event to be dispatched
716      * @return {@code false} if at least one of the event handlers which handled the event
717      *         called <code>preventDefault</code>; {@code true} otherwise
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      * {@inheritDoc}
729      */
730     @Override
731     public Selection getSelection() {
732         return getWindow().getSelectionImpl();
733     }
734 
735     /**
736      * Creates a new HTML attribute with the specified name.
737      *
738      * @param attributeName the name of the attribute to create
739      * @return an attribute with the specified name
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      * {@inheritDoc}
753      */
754     @Override
755     public String getBaseURI() {
756         return getPage().getBaseURL().toString();
757     }
758 
759     /**
760      * {@inheritDoc}
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 }