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