View Javadoc
1   /*
2    * Copyright (c) 2002-2017 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    * http://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 com.gargoylesoftware.htmlunit.javascript.host.html;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLDOCUMENT_COLOR;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLDOCUMENT_FUNCTION_DETACHED;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLDOCUMENT_GET_ALSO_FRAMES;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLDOCUMENT_GET_FOR_ID_AND_OR_NAME;
21  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLDOCUMENT_GET_PREFERS_STANDARD_FUNCTIONS;
22  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTML_COLOR_EXPAND_ZERO;
23  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOCUMENT_CREATE_ATTRUBUTE_LOWER_CASE;
24  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
25  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
26  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
27  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
28  
29  import java.io.IOException;
30  import java.net.URL;
31  import java.util.ArrayList;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Set;
35  
36  import org.apache.commons.lang3.StringUtils;
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  
40  import com.gargoylesoftware.htmlunit.ScriptResult;
41  import com.gargoylesoftware.htmlunit.StringWebResponse;
42  import com.gargoylesoftware.htmlunit.WebClient;
43  import com.gargoylesoftware.htmlunit.WebWindow;
44  import com.gargoylesoftware.htmlunit.html.BaseFrameElement;
45  import com.gargoylesoftware.htmlunit.html.DomElement;
46  import com.gargoylesoftware.htmlunit.html.DomNode;
47  import com.gargoylesoftware.htmlunit.html.FrameWindow;
48  import com.gargoylesoftware.htmlunit.html.HtmlApplet;
49  import com.gargoylesoftware.htmlunit.html.HtmlAttributeChangeEvent;
50  import com.gargoylesoftware.htmlunit.html.HtmlElement;
51  import com.gargoylesoftware.htmlunit.html.HtmlForm;
52  import com.gargoylesoftware.htmlunit.html.HtmlImage;
53  import com.gargoylesoftware.htmlunit.html.HtmlInlineFrame;
54  import com.gargoylesoftware.htmlunit.html.HtmlPage;
55  import com.gargoylesoftware.htmlunit.html.HtmlScript;
56  import com.gargoylesoftware.htmlunit.httpclient.HtmlUnitBrowserCompatCookieSpec;
57  import com.gargoylesoftware.htmlunit.javascript.PostponedAction;
58  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
59  import com.gargoylesoftware.htmlunit.javascript.configuration.CanSetReadOnly;
60  import com.gargoylesoftware.htmlunit.javascript.configuration.CanSetReadOnlyStatus;
61  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
62  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
63  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
64  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
65  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
66  import com.gargoylesoftware.htmlunit.javascript.host.Element;
67  import com.gargoylesoftware.htmlunit.javascript.host.Window;
68  import com.gargoylesoftware.htmlunit.javascript.host.dom.Attr;
69  import com.gargoylesoftware.htmlunit.javascript.host.dom.Document;
70  import com.gargoylesoftware.htmlunit.javascript.host.dom.Selection;
71  import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
72  import com.gargoylesoftware.htmlunit.util.Cookie;
73  
74  import net.sourceforge.htmlunit.corejs.javascript.Context;
75  import net.sourceforge.htmlunit.corejs.javascript.Function;
76  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
77  import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
78  
79  /**
80   * A JavaScript object for {@code HTMLDocument}.
81   *
82   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
83   * @author David K. Taylor
84   * @author <a href="mailto:chen_jun@users.sourceforge.net">Chen Jun</a>
85   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
86   * @author Chris Erskine
87   * @author Marc Guillemot
88   * @author Daniel Gredler
89   * @author Michael Ottati
90   * @author <a href="mailto:george@murnock.com">George Murnock</a>
91   * @author Ahmed Ashour
92   * @author Rob Di Marco
93   * @author Sudhan Moghe
94   * @author <a href="mailto:mike@10gen.com">Mike Dirolf</a>
95   * @author Ronald Brill
96   * @author Frank Danek
97   * @see <a href="http://msdn.microsoft.com/en-us/library/ms535862.aspx">MSDN documentation</a>
98   * @see <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-html.html#ID-7068919">
99   * W3C DOM Level 1</a>
100  */
101 @JsxClass
102 public class HTMLDocument extends Document {
103 
104     private static final Log LOG = LogFactory.getLog(HTMLDocument.class);
105 
106     private enum ParsingStatus { OUTSIDE, START, IN_NAME, INSIDE, IN_STRING }
107 
108     private HTMLElement activeElement_;
109 
110     /** The buffer that will be used for calls to document.write(). */
111     private final StringBuilder writeBuilder_ = new StringBuilder();
112     private boolean writeInCurrentDocument_ = true;
113 
114     private boolean closePostponedAction_;
115 
116     /**
117      * The constructor.
118      */
119     @JsxConstructor({CHROME, FF, EDGE})
120     public HTMLDocument() {
121     }
122 
123     /**
124      * {@inheritDoc}
125      */
126     @Override
127     public DomNode getDomNodeOrDie() {
128         try {
129             return super.getDomNodeOrDie();
130         }
131         catch (final IllegalStateException e) {
132             throw Context.reportRuntimeError("No node attached to this object");
133         }
134     }
135 
136     /**
137      * Returns the HTML page that this document is modeling.
138      * @return the HTML page that this document is modeling
139      */
140     @Override
141     public HtmlPage getPage() {
142         return (HtmlPage) getDomNodeOrDie();
143     }
144 
145     /**
146      * {@inheritDoc}
147      */
148     @Override
149     @JsxGetter(FF)
150     public Object getForms() {
151         return super.getForms();
152     }
153 
154     /**
155      * {@inheritDoc}
156      */
157     @Override
158     @JsxGetter(FF)
159     public Object getEmbeds() {
160         return super.getEmbeds();
161     }
162 
163     /**
164      * {@inheritDoc}
165      */
166     @Override
167     @JsxGetter(FF)
168     public Object getPlugins() {
169         return super.getPlugins();
170     }
171 
172     /**
173      * {@inheritDoc}
174      */
175     @Override
176     @JsxGetter(FF)
177     public Object getLinks() {
178         return super.getLinks();
179     }
180 
181     /**
182      * {@inheritDoc}
183      */
184     @Override
185     @JsxGetter(FF)
186     public Object getAnchors() {
187         return super.getAnchors();
188     }
189 
190     /**
191      * {@inheritDoc}
192      */
193     @Override
194     @JsxGetter(FF)
195     public Object getApplets() {
196         return super.getApplets();
197     }
198 
199     /**
200      * JavaScript function "write" may accept a variable number of arguments.
201      * @param context the JavaScript context
202      * @param thisObj the scriptable
203      * @param args the arguments passed into the method
204      * @param function the function
205      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536782.aspx">MSDN documentation</a>
206      */
207     @JsxFunction
208     public static void write(final Context context, final Scriptable thisObj, final Object[] args,
209         final Function function) {
210         final HTMLDocument thisAsDocument = getDocument(thisObj);
211         thisAsDocument.write(concatArgsAsString(args));
212     }
213 
214     /**
215      * Converts the arguments to strings and concatenate them.
216      * @param args the JavaScript arguments
217      * @return the string concatenation
218      */
219     private static String concatArgsAsString(final Object[] args) {
220         final StringBuilder builder = new StringBuilder();
221         for (final Object arg : args) {
222             builder.append(Context.toString(arg));
223         }
224         return builder.toString();
225     }
226 
227     /**
228      * JavaScript function "writeln" may accept a variable number of arguments.
229      * @param context the JavaScript context
230      * @param thisObj the scriptable
231      * @param args the arguments passed into the method
232      * @param function the function
233      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536783.aspx">MSDN documentation</a>
234      */
235     @JsxFunction
236     public static void writeln(
237         final Context context, final Scriptable thisObj, final Object[] args, final Function function) {
238         final HTMLDocument thisAsDocument = getDocument(thisObj);
239         thisAsDocument.write(concatArgsAsString(args) + "\n");
240     }
241 
242     /**
243      * Returns the current document instance, using <tt>thisObj</tt> as a hint.
244      * @param thisObj a hint as to the current document (may be the prototype when function is used without "this")
245      * @return the current document instance
246      */
247     private static HTMLDocument getDocument(final Scriptable thisObj) {
248         // if function is used "detached", then thisObj is the top scope (ie Window), not the real object
249         // cf unit test DocumentTest#testDocumentWrite_AssignedToVar
250         // may be the prototype too
251         // cf DocumentTest#testDocumentWrite_AssignedToVar2
252         if (thisObj instanceof HTMLDocument && thisObj.getPrototype() instanceof HTMLDocument) {
253             return (HTMLDocument) thisObj;
254         }
255         if (thisObj instanceof DocumentProxy && thisObj.getPrototype() instanceof HTMLDocument) {
256             return (HTMLDocument) ((DocumentProxy) thisObj).getDelegee();
257         }
258 
259         final Window window = getWindow(thisObj);
260         if (window.getBrowserVersion().hasFeature(HTMLDOCUMENT_FUNCTION_DETACHED)) {
261             return (HTMLDocument) window.getDocument();
262         }
263         throw Context.reportRuntimeError("Function can't be used detached from document");
264     }
265 
266     private boolean executionExternalPostponed_;
267 
268     /**
269      * This a hack!!! A cleaner way is welcome.
270      * Handle a case where document.write is simply ignored.
271      * See HTMLDocumentWrite2Test.write_fromScriptAddedWithAppendChild_external.
272      * @param executing indicates if executing or not
273      */
274     public void setExecutingDynamicExternalPosponed(final boolean executing) {
275         executionExternalPostponed_ = executing;
276     }
277 
278     /**
279      * JavaScript function "write".
280      *
281      * See http://www.whatwg.org/specs/web-apps/current-work/multipage/section-dynamic.html for
282      * a good description of the semantics of open(), write(), writeln() and close().
283      *
284      * @param content the content to write
285      */
286     protected void write(final String content) {
287         // really strange: if called from an external script loaded as postponed action, write is ignored!!!
288         if (executionExternalPostponed_) {
289             if (LOG.isDebugEnabled()) {
290                 LOG.debug("skipping write for external posponed: " + content);
291             }
292             return;
293         }
294 
295         if (LOG.isDebugEnabled()) {
296             LOG.debug("write: " + content);
297         }
298 
299         final HtmlPage page = (HtmlPage) getDomNodeOrDie();
300         if (!page.isBeingParsed()) {
301             writeInCurrentDocument_ = false;
302         }
303 
304         // Add content to the content buffer.
305         writeBuilder_.append(content);
306 
307         // If open() was called; don't write to doc yet -- wait for call to close().
308         if (!writeInCurrentDocument_) {
309             if (LOG.isDebugEnabled()) {
310                 LOG.debug("wrote content to buffer");
311             }
312             scheduleImplicitClose();
313             return;
314         }
315         final String bufferedContent = writeBuilder_.toString();
316         if (!canAlreadyBeParsed(bufferedContent)) {
317             if (LOG.isDebugEnabled()) {
318                 LOG.debug("write: not enough content to parse it now");
319             }
320             return;
321         }
322 
323         writeBuilder_.setLength(0);
324         page.writeInParsedStream(bufferedContent);
325     }
326 
327     private void scheduleImplicitClose() {
328         if (!closePostponedAction_) {
329             closePostponedAction_ = true;
330             final HtmlPage page = (HtmlPage) getDomNodeOrDie();
331             final WebWindow enclosingWindow = page.getEnclosingWindow();
332             page.getWebClient().getJavaScriptEngine().addPostponedAction(new PostponedAction(page) {
333                 @Override
334                 public void execute() throws Exception {
335                     if (writeBuilder_.length() != 0) {
336                         close();
337                     }
338                     closePostponedAction_ = false;
339                 }
340 
341                 @Override
342                 public boolean isStillAlive() {
343                     return !enclosingWindow.isClosed();
344                 }
345             });
346         }
347     }
348 
349     /**
350      * Indicates if the content is a well formed HTML snippet that can already be parsed to be added to the DOM.
351      *
352      * @param content the HTML snippet
353      * @return {@code false} if it not well formed
354      */
355     static boolean canAlreadyBeParsed(final String content) {
356         // all <script> must have their </script> because the parser doesn't close automatically this tag
357         // All tags must be complete, that is from '<' to '>'.
358         ParsingStatus tagState = ParsingStatus.OUTSIDE;
359         int tagNameBeginIndex = 0;
360         int scriptTagCount = 0;
361         boolean tagIsOpen = true;
362         char stringBoundary = 0;
363         boolean stringSkipNextChar = false;
364         int index = 0;
365         char openingQuote = 0;
366         for (final char currentChar : content.toCharArray()) {
367             switch (tagState) {
368                 case OUTSIDE:
369                     if (currentChar == '<') {
370                         tagState = ParsingStatus.START;
371                         tagIsOpen = true;
372                     }
373                     else if (scriptTagCount > 0 && (currentChar == '\'' || currentChar == '"')) {
374                         tagState = ParsingStatus.IN_STRING;
375                         stringBoundary = currentChar;
376                         stringSkipNextChar = false;
377                     }
378                     break;
379                 case START:
380                     if (currentChar == '/') {
381                         tagIsOpen = false;
382                         tagNameBeginIndex = index + 1;
383                     }
384                     else {
385                         tagNameBeginIndex = index;
386                     }
387                     tagState = ParsingStatus.IN_NAME;
388                     break;
389                 case IN_NAME:
390                     if (Character.isWhitespace(currentChar) || currentChar == '>') {
391                         final String tagName = content.substring(tagNameBeginIndex, index);
392                         if ("script".equalsIgnoreCase(tagName)) {
393                             if (tagIsOpen) {
394                                 scriptTagCount++;
395                             }
396                             else if (scriptTagCount > 0) {
397                                 // Ignore extra close tags for now. Let the parser deal with them.
398                                 scriptTagCount--;
399                             }
400                         }
401                         if (currentChar == '>') {
402                             tagState = ParsingStatus.OUTSIDE;
403                         }
404                         else {
405                             tagState = ParsingStatus.INSIDE;
406                         }
407                     }
408                     else if (!Character.isLetter(currentChar)) {
409                         tagState = ParsingStatus.OUTSIDE;
410                     }
411                     break;
412                 case INSIDE:
413                     if (currentChar == openingQuote) {
414                         openingQuote = 0;
415                     }
416                     else if (openingQuote == 0) {
417                         if (currentChar == '\'' || currentChar == '"') {
418                             openingQuote = currentChar;
419                         }
420                         else if (currentChar == '>' && openingQuote == 0) {
421                             tagState = ParsingStatus.OUTSIDE;
422                         }
423                     }
424                     break;
425                 case IN_STRING:
426                     if (stringSkipNextChar) {
427                         stringSkipNextChar = false;
428                     }
429                     else {
430                         if (currentChar == stringBoundary) {
431                             tagState = ParsingStatus.OUTSIDE;
432                         }
433                         else if (currentChar == '\\') {
434                             stringSkipNextChar = true;
435                         }
436                     }
437                     break;
438                 default:
439                     // nothing
440             }
441             index++;
442         }
443         if (scriptTagCount > 0 || tagState != ParsingStatus.OUTSIDE) {
444             if (LOG.isDebugEnabled()) {
445                 final StringBuilder message = new StringBuilder();
446                 message.append("canAlreadyBeParsed() retruns false for content: '");
447                 message.append(StringUtils.abbreviateMiddle(content, ".", 100));
448                 message.append("' (scriptTagCount: " + scriptTagCount);
449                 message.append(" tagState: " + tagState);
450                 message.append(")");
451                 LOG.debug(message.toString());
452             }
453             return false;
454         }
455 
456         return true;
457     }
458 
459     /**
460      * Gets the node that is the last one when exploring following nodes, depth-first.
461      * @param node the node to search
462      * @return the searched node
463      */
464     HtmlElement getLastHtmlElement(final HtmlElement node) {
465         final DomNode lastChild = node.getLastChild();
466         if (lastChild == null
467                 || !(lastChild instanceof HtmlElement)
468                 || lastChild instanceof HtmlScript) {
469             return node;
470         }
471 
472         return getLastHtmlElement((HtmlElement) lastChild);
473     }
474 
475     /**
476      * {@inheritDoc}
477      */
478     @Override
479     @JsxGetter
480     public String getCookie() {
481         final HtmlPage page = getPage();
482 
483         final URL url = page.getUrl();
484 
485         final StringBuilder builder = new StringBuilder();
486         final Set<Cookie> cookies = page.getWebClient().getCookies(url);
487         for (final Cookie cookie : cookies) {
488             if (cookie.isHttpOnly()) {
489                 continue;
490             }
491             if (builder.length() != 0) {
492                 builder.append("; ");
493             }
494             if (!HtmlUnitBrowserCompatCookieSpec.EMPTY_COOKIE_NAME.equals(cookie.getName())) {
495                 builder.append(cookie.getName());
496                 builder.append("=");
497             }
498             builder.append(cookie.getValue());
499         }
500 
501         return builder.toString();
502     }
503 
504     /**
505      * Adds a cookie, as long as cookies are enabled.
506      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533693.aspx">MSDN documentation</a>
507      * @param newCookie in the format "name=value[;expires=date][;domain=domainname][;path=path][;secure]
508      */
509     @JsxSetter
510     public void setCookie(final String newCookie) {
511         final HtmlPage page = getPage();
512         final WebClient client = page.getWebClient();
513 
514         client.addCookie(newCookie, getPage().getUrl(), this);
515     }
516 
517     /**
518      * {@inheritDoc}
519      */
520     @Override
521     @JsxGetter(FF)
522     public Object getImages() {
523         return super.getImages();
524     }
525 
526     /**
527      * Returns the value of the {@code all} property.
528      * @return the value of the {@code all} property
529      */
530     @JsxGetter
531     public HTMLCollection getAll() {
532         return new HTMLAllCollection(getDomNodeOrDie()) {
533             @Override
534             protected boolean isMatching(final DomNode node) {
535                 return true;
536             }
537 
538             @Override
539             public boolean avoidObjectDetection() {
540                 return true;
541             }
542         };
543     }
544 
545     /**
546      * JavaScript function "open".
547      *
548      * See http://www.whatwg.org/specs/web-apps/current-work/multipage/section-dynamic.html for
549      * a good description of the semantics of open(), write(), writeln() and close().
550      *
551      * @param url when a new document is opened, <i>url</i> is a String that specifies a MIME type for the document.
552      *        When a new window is opened, <i>url</i> is a String that specifies the URL to render in the new window
553      * @param name the name
554      * @param features the features
555      * @param replace whether to replace in the history list or no
556      * @return a reference to the new document object.
557      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536652.aspx">MSDN documentation</a>
558      */
559     @JsxFunction
560     public Object open(final Object url, final Object name, final Object features,
561             final Object replace) {
562         // Any open() invocations are ignored during the parsing stage, because write() and
563         // writeln() invocations will directly append content to the current insertion point.
564         final HtmlPage page = getPage();
565         if (page.isBeingParsed()) {
566             LOG.warn("Ignoring call to open() during the parsing stage.");
567             return null;
568         }
569 
570         // We're not in the parsing stage; OK to continue.
571         if (!writeInCurrentDocument_) {
572             LOG.warn("Function open() called when document is already open.");
573         }
574         writeInCurrentDocument_ = false;
575         if (getWindow().getWebWindow() instanceof FrameWindow
576                 && WebClient.ABOUT_BLANK.equals(getPage().getUrl().toExternalForm())) {
577             final URL enclosingUrl = ((FrameWindow) getWindow().getWebWindow()).getEnclosingPage().getUrl();
578             getPage().getWebResponse().getWebRequest().setUrl(enclosingUrl);
579         }
580         return this;
581     }
582 
583     /**
584      * {@inheritDoc}
585      */
586     @Override
587     @JsxFunction(FF)
588     public void close() throws IOException {
589         if (writeInCurrentDocument_) {
590             LOG.warn("close() called when document is not open.");
591         }
592         else {
593             final HtmlPage page = getPage();
594             final URL url = page.getUrl();
595             final StringWebResponse webResponse = new StringWebResponse(writeBuilder_.toString(), url);
596             webResponse.setFromJavascript(true);
597             writeInCurrentDocument_ = true;
598             writeBuilder_.setLength(0);
599 
600             final WebClient webClient = page.getWebClient();
601             final WebWindow window = page.getEnclosingWindow();
602             // reset isAttachedToPageDuringOnload_ to trigger the onload event for chrome also
603             if (window instanceof FrameWindow) {
604                 final BaseFrameElement frame = ((FrameWindow) window).getFrameElement();
605                 final ScriptableObject scriptable = frame.getScriptableObject();
606                 if (scriptable instanceof HTMLIFrameElement) {
607                     ((HTMLIFrameElement) scriptable).onRefresh();
608                 }
609             }
610             webClient.loadWebResponseInto(webResponse, window);
611         }
612     }
613 
614     /**
615      * {@inheritDoc}
616      */
617     @JsxGetter
618     public Element getDocumentElement() {
619         implicitCloseIfNecessary();
620         return super.getDocumentElement();
621     }
622 
623     /**
624      * {@inheritDoc}
625      */
626     @Override
627     @JsxFunction(FF)
628     public boolean execCommand(final String cmd, final boolean userInterface, final Object value) {
629         return super.execCommand(cmd, userInterface, value);
630     }
631 
632     /**
633      * {@inheritDoc}
634      */
635     @Override
636     @JsxFunction(FF)
637     public boolean queryCommandEnabled(final String cmd) {
638         return super.queryCommandEnabled(cmd);
639     }
640 
641     /**
642      * {@inheritDoc}
643      */
644     @Override
645     @JsxFunction(FF)
646     public boolean queryCommandSupported(final String cmd) {
647         return super.queryCommandSupported(cmd);
648     }
649 
650     /**
651      * Closes the document implicitly, i.e. flushes the <tt>document.write</tt> buffer (IE only).
652      */
653     private void implicitCloseIfNecessary() {
654         if (!writeInCurrentDocument_) {
655             try {
656                 close();
657             }
658             catch (final IOException e) {
659                 throw Context.throwAsScriptRuntimeEx(e);
660             }
661         }
662     }
663 
664     /**
665      * Gets the window in which this document is contained.
666      * @return the window
667      */
668     @JsxGetter(IE)
669     public Object getParentWindow() {
670         return getWindow();
671     }
672 
673     /**
674      * {@inheritDoc}
675      */
676     @Override
677     public Object appendChild(final Object childObject) {
678         throw Context.reportRuntimeError("Node cannot be inserted at the specified point in the hierarchy.");
679     }
680 
681     /**
682      * Returns the element with the specified ID, or {@code null} if that element could not be found.
683      * @param id the ID to search for
684      * @return the element, or {@code null} if it could not be found
685      */
686     @JsxFunction
687     public Object getElementById(final String id) {
688         implicitCloseIfNecessary();
689         Object result = null;
690         final DomElement domElement = getPage().getElementById(id);
691         if (null == domElement) {
692             // Just fall through - result is already set to null
693             if (LOG.isDebugEnabled()) {
694                 LOG.debug("getElementById(" + id + "): no DOM node found with this id");
695             }
696         }
697         else {
698             final Object jsElement = getScriptableFor(domElement);
699             if (jsElement == NOT_FOUND) {
700                 if (LOG.isDebugEnabled()) {
701                     LOG.debug("getElementById(" + id
702                             + ") cannot return a result as there isn't a JavaScript object for the HTML element "
703                             + domElement.getClass().getName());
704                 }
705             }
706             else {
707                 result = jsElement;
708             }
709         }
710         return result;
711     }
712 
713     /**
714      * {@inheritDoc}
715      */
716     @Override
717     public HTMLCollection getElementsByClassName(final String className) {
718         return ((HTMLElement) getDocumentElement()).getElementsByClassName(className);
719     }
720 
721     /**
722      * {@inheritDoc}
723      */
724     @Override
725     @JsxFunction(FF)
726     public HTMLCollection getElementsByName(final String elementName) {
727         implicitCloseIfNecessary();
728         if ("null".equals(elementName)) {
729             return HTMLCollection.emptyCollection(getWindow().getDomNodeOrDie());
730         }
731         // Null must me changed to '' for proper collection initialization.
732         final String expElementName = "null".equals(elementName) ? "" : elementName;
733 
734         final HtmlPage page = getPage();
735         final HTMLCollection collection = new HTMLCollection(page, true) {
736             @Override
737             protected List<DomNode> computeElements() {
738                 return new ArrayList<>(page.getElementsByName(expElementName));
739             }
740 
741             @Override
742             protected EffectOnCache getEffectOnCache(final HtmlAttributeChangeEvent event) {
743                 if ("name".equals(event.getName())) {
744                     return EffectOnCache.RESET;
745                 }
746                 return EffectOnCache.NONE;
747             }
748         };
749 
750         return collection;
751     }
752 
753     /**
754      * Calls to <tt>document.XYZ</tt> should first look at elements named <tt>XYZ</tt> before
755      * using standard functions.
756      *
757      * {@inheritDoc}
758      */
759     @Override
760     protected Object getWithPreemption(final String name) {
761         final HtmlPage page = (HtmlPage) getDomNodeOrNull();
762         if (page == null || getBrowserVersion().hasFeature(HTMLDOCUMENT_GET_PREFERS_STANDARD_FUNCTIONS)) {
763             final Object response = getPrototype().get(name, this);
764             if (response != NOT_FOUND) {
765                 return response;
766             }
767         }
768         return getIt(name);
769     }
770 
771     private Object getIt(final String name) {
772         final HtmlPage page = (HtmlPage) getDomNodeOrNull();
773 
774         final boolean forIDAndOrName = getBrowserVersion().hasFeature(HTMLDOCUMENT_GET_FOR_ID_AND_OR_NAME);
775         final boolean alsoFrames = getBrowserVersion().hasFeature(HTMLDOCUMENT_GET_ALSO_FRAMES);
776         final HTMLCollection collection = new HTMLCollection(page, true) {
777             @Override
778             protected List<DomNode> computeElements() {
779                 final List<DomElement> elements;
780                 if (forIDAndOrName) {
781                     elements = page.getElementsByIdAndOrName(name);
782                 }
783                 else {
784                     elements = page.getElementsByName(name);
785                 }
786                 final List<DomNode> matchingElements = new ArrayList<>();
787                 for (final DomElement elt : elements) {
788                     if (elt instanceof HtmlForm || elt instanceof HtmlImage || elt instanceof HtmlApplet
789                             || (alsoFrames && elt instanceof BaseFrameElement)) {
790                         matchingElements.add(elt);
791                     }
792                 }
793                 return matchingElements;
794             }
795 
796             @Override
797             protected EffectOnCache getEffectOnCache(final HtmlAttributeChangeEvent event) {
798                 final String attributeName = event.getName();
799                 if ("name".equals(attributeName)) {
800                     return EffectOnCache.RESET;
801                 }
802                 else if (forIDAndOrName && "id".equals(attributeName)) {
803                     return EffectOnCache.RESET;
804                 }
805 
806                 return EffectOnCache.NONE;
807             }
808 
809             @Override
810             protected SimpleScriptable getScriptableFor(final Object object) {
811                 if (alsoFrames && object instanceof BaseFrameElement) {
812                     return (SimpleScriptable) ((BaseFrameElement) object).getEnclosedWindow().getScriptableObject();
813                 }
814                 return super.getScriptableFor(object);
815             }
816         };
817 
818         final int length = collection.getLength();
819         if (length == 0) {
820             return NOT_FOUND;
821         }
822         else if (length == 1) {
823             return collection.item(Integer.valueOf(0));
824         }
825 
826         return collection;
827     }
828 
829     /**
830      * {@inheritDoc}
831      */
832     @Override
833     @JsxGetter
834     public HTMLElement getHead() {
835         final HtmlElement head = getPage().getHead();
836         if (head != null) {
837             return (HTMLElement) head.getScriptableObject();
838         }
839         return null;
840     }
841 
842     /**
843      * {@inheritDoc}
844      */
845     @Override
846     @JsxGetter(FF)
847     @CanSetReadOnly(CanSetReadOnlyStatus.EXCEPTION)
848     public HTMLElement getBody() {
849         return super.getBody();
850     }
851 
852     /**
853      * {@inheritDoc}
854      */
855     @Override
856     public String getTitle() {
857         return getPage().getTitleText();
858     }
859 
860     /**
861      * {@inheritDoc}
862      */
863     @Override
864     public void setTitle(final String title) {
865         getPage().setTitleText(title);
866     }
867 
868     /**
869      * {@inheritDoc}
870      */
871     @Override
872     @JsxGetter({CHROME, FF})
873     public String getBgColor() {
874         String color = getPage().getBody().getAttribute("bgColor");
875         if (color == DomElement.ATTRIBUTE_NOT_DEFINED && getBrowserVersion().hasFeature(HTMLDOCUMENT_COLOR)) {
876             color = "#ffffff";
877         }
878         if (getBrowserVersion().hasFeature(HTML_COLOR_EXPAND_ZERO) && "#0".equals(color)) {
879             color = "#000000";
880         }
881         return color;
882     }
883 
884     /**
885      * {@inheritDoc}
886      */
887     @Override
888     @JsxSetter({CHROME, FF})
889     public void setBgColor(final String color) {
890         final HTMLBodyElement body = (HTMLBodyElement) getPage().getBody().getScriptableObject();
891         body.setBgColor(color);
892     }
893 
894     /**
895      * {@inheritDoc}
896      */
897     @Override
898     @JsxGetter({CHROME, FF})
899     public String getAlinkColor() {
900         String color = getPage().getBody().getAttribute("aLink");
901         if (color == DomElement.ATTRIBUTE_NOT_DEFINED && getBrowserVersion().hasFeature(HTMLDOCUMENT_COLOR)) {
902             color = "#0000ff";
903         }
904         if (getBrowserVersion().hasFeature(HTML_COLOR_EXPAND_ZERO) && "#0".equals(color)) {
905             color = "#000000";
906         }
907         return color;
908     }
909 
910     /**
911      * {@inheritDoc}
912      */
913     @Override
914     @JsxSetter({CHROME, FF})
915     public void setAlinkColor(final String color) {
916         final HTMLBodyElement body = (HTMLBodyElement) getPage().getBody().getScriptableObject();
917         body.setALink(color);
918     }
919 
920     /**
921      * {@inheritDoc}
922      */
923     @Override
924     @JsxGetter({CHROME, FF})
925     public String getLinkColor() {
926         String color = getPage().getBody().getAttribute("link");
927         if (color == DomElement.ATTRIBUTE_NOT_DEFINED && getBrowserVersion().hasFeature(HTMLDOCUMENT_COLOR)) {
928             color = "#0000ff";
929         }
930         if (getBrowserVersion().hasFeature(HTML_COLOR_EXPAND_ZERO) && "#0".equals(color)) {
931             color = "#000000";
932         }
933         return color;
934     }
935 
936     /**
937      * {@inheritDoc}
938      */
939     @Override
940     @JsxSetter({CHROME, FF})
941     public void setLinkColor(final String color) {
942         final HTMLBodyElement body = (HTMLBodyElement) getPage().getBody().getScriptableObject();
943         body.setLink(color);
944     }
945 
946     /**
947      * {@inheritDoc}
948      */
949     @Override
950     @JsxGetter({CHROME, FF})
951     public String getVlinkColor() {
952         String color = getPage().getBody().getAttribute("vLink");
953         if (color == DomElement.ATTRIBUTE_NOT_DEFINED && getBrowserVersion().hasFeature(HTMLDOCUMENT_COLOR)) {
954             color = "#800080";
955         }
956         if (getBrowserVersion().hasFeature(HTML_COLOR_EXPAND_ZERO) && "#0".equals(color)) {
957             color = "#000000";
958         }
959         return color;
960     }
961 
962     /**
963      * {@inheritDoc}
964      */
965     @Override
966     @JsxSetter({CHROME, FF})
967     public void setVlinkColor(final String color) {
968         final HTMLBodyElement body = (HTMLBodyElement) getPage().getBody().getScriptableObject();
969         body.setVLink(color);
970     }
971 
972     /**
973      * {@inheritDoc}
974      */
975     @Override
976     @JsxGetter({CHROME, FF})
977     public String getFgColor() {
978         String color = getPage().getBody().getAttribute("text");
979         if (color == DomElement.ATTRIBUTE_NOT_DEFINED && getBrowserVersion().hasFeature(HTMLDOCUMENT_COLOR)) {
980             color = "#000000";
981         }
982         if (getBrowserVersion().hasFeature(HTML_COLOR_EXPAND_ZERO) && "#0".equals(color)) {
983             color = "#000000";
984         }
985         return color;
986     }
987 
988     /**
989      * {@inheritDoc}
990      */
991     @Override
992     @JsxSetter({CHROME, FF})
993     public void setFgColor(final String color) {
994         final HTMLBodyElement body = (HTMLBodyElement) getPage().getBody().getScriptableObject();
995         body.setText(color);
996     }
997 
998     /**
999      * {@inheritDoc}
1000      */
1001     @Override
1002     @JsxGetter(FF)
1003     public String getDomain() {
1004         return super.getDomain();
1005     }
1006 
1007     /**
1008      * {@inheritDoc}
1009      */
1010     @Override
1011     @JsxSetter(FF)
1012     public void setDomain(final String newDomain) {
1013         super.setDomain(newDomain);
1014     }
1015 
1016     /**
1017      * {@inheritDoc}
1018      */
1019     @Override
1020     @JsxGetter(FF)
1021     public Object getScripts() {
1022         return super.getScripts();
1023     }
1024 
1025     /**
1026      * {@inheritDoc}
1027      */
1028     @Override
1029     public HTMLElement getActiveElement() {
1030         if (activeElement_ == null) {
1031             final HtmlElement body = getPage().getBody();
1032             if (body != null) {
1033                 activeElement_ = (HTMLElement) getScriptableFor(body);
1034             }
1035         }
1036 
1037         return activeElement_;
1038     }
1039 
1040     /**
1041      * {@inheritDoc}
1042      */
1043     @Override
1044     public boolean hasFocus() {
1045         return activeElement_ != null && getPage().getFocusedElement() == activeElement_.getDomNodeOrDie();
1046     }
1047 
1048     /**
1049      * Sets the specified element as the document's active element.
1050      * @see HTMLElement#setActive()
1051      * @param element the new active element for this document
1052      */
1053     public void setActiveElement(final HTMLElement element) {
1054         // TODO update page focus element also
1055 
1056         activeElement_ = element;
1057 
1058         if (element != null) {
1059             // if this is part of an iFrame, make the iFrame tag the
1060             // active element of his doc
1061             final WebWindow window = element.getDomNodeOrDie().getPage().getEnclosingWindow();
1062             if (window instanceof FrameWindow) {
1063                 final BaseFrameElement frame = ((FrameWindow) window).getFrameElement();
1064                 if (frame instanceof HtmlInlineFrame) {
1065                     final Window winWithFrame = (Window) frame.getPage().getEnclosingWindow().getScriptableObject();
1066                     ((HTMLDocument) winWithFrame.getDocument()).setActiveElement(
1067                                 (HTMLElement) frame.getScriptableObject());
1068                 }
1069             }
1070         }
1071     }
1072 
1073     /**
1074      * Dispatches an event into the event system (standards-conformant browsers only). See
1075      * <a href="https://developer.mozilla.org/en-US/docs/DOM/element.dispatchEvent">the Gecko
1076      * DOM reference</a> for more information.
1077      *
1078      * @param event the event to be dispatched
1079      * @return {@code false} if at least one of the event handlers which handled the event
1080      *         called <tt>preventDefault</tt>; {@code true} otherwise
1081      */
1082     @Override
1083     @JsxFunction
1084     public boolean dispatchEvent(final Event event) {
1085         event.setTarget(this);
1086         final ScriptResult result = fireEvent(event);
1087         return !event.isAborted(result);
1088     }
1089 
1090     /**
1091      * Does... nothing.
1092      * @see <a href="https://developer.mozilla.org/en/DOM/document.clear">Mozilla doc</a>
1093      */
1094     @JsxFunction
1095     public void clear() {
1096         // nothing
1097     }
1098 
1099     /**
1100      * Sets the head.
1101      * @param head the head
1102      */
1103     @JsxSetter({FF, IE})
1104     public void setHead(final ScriptableObject head) {
1105         //ignore
1106     }
1107 
1108     /**
1109      * {@inheritDoc}
1110      */
1111     @Override
1112     @JsxFunction
1113     public Selection getSelection() {
1114         return getWindow().getSelectionImpl();
1115     }
1116 
1117     /**
1118      * Creates a new HTML attribute with the specified name.
1119      *
1120      * @param attributeName the name of the attribute to create
1121      * @return an attribute with the specified name
1122      */
1123     @Override
1124     public Attr createAttribute(final String attributeName) {
1125         String name = attributeName;
1126         if (StringUtils.isNotEmpty(name)
1127                 && getBrowserVersion().hasFeature(JS_DOCUMENT_CREATE_ATTRUBUTE_LOWER_CASE)) {
1128             name = name.toLowerCase(Locale.ROOT);
1129         }
1130 
1131         return super.createAttribute(name);
1132     }
1133 
1134     /**
1135      * {@inheritDoc}
1136      */
1137     @Override
1138     public String getBaseURI() {
1139         return getPage().getBaseURL().toString();
1140     }
1141 
1142     /**
1143      * {@inheritDoc}
1144      */
1145     @Override
1146     @JsxFunction({CHROME, FF})
1147     public void captureEvents(final String type) {
1148         // Empty.
1149     }
1150 
1151     /**
1152      * {@inheritDoc}
1153      */
1154     @Override
1155     @JsxFunction({CHROME, FF})
1156     public void releaseEvents(final String type) {
1157         // Empty.
1158     }
1159 
1160     /**
1161      * {@inheritDoc}
1162      */
1163     @Override
1164     @JsxGetter(FF)
1165     public String getDesignMode() {
1166         return super.getDesignMode();
1167     }
1168 
1169     /**
1170      * {@inheritDoc}
1171      */
1172     @Override
1173     @JsxSetter(FF)
1174     public void setDesignMode(final String mode) {
1175         super.setDesignMode(mode);
1176     }
1177 
1178     /**
1179      * {@inheritDoc}
1180      */
1181     @Override
1182     public Object elementFromPoint(final int x, final int y) {
1183         final HtmlElement element = getPage().getElementFromPoint(x, y);
1184         return element == null ? null : element.getScriptableObject();
1185     }
1186 }