View Javadoc
1   /*
2    * Copyright (c) 2002-2014 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;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOCUMENT_CREATE_ELEMENT_EXTENDED_SYNTAX;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOCUMENT_DESIGN_MODE_CAPITAL_FIRST;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOCUMENT_DESIGN_MODE_INHERIT;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_DOCUMENT_DESIGN_MODE_ONLY_FOR_FRAMES;
21  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_GET_ELEMENTS_BY_TAG_NAME_NOT_SUPPORTS_NAMESPACES;
22  import static com.gargoylesoftware.htmlunit.javascript.configuration.BrowserName.CHROME;
23  import static com.gargoylesoftware.htmlunit.javascript.configuration.BrowserName.FF;
24  import static com.gargoylesoftware.htmlunit.javascript.configuration.BrowserName.IE;
25  
26  import java.io.IOException;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import net.sourceforge.htmlunit.corejs.javascript.Context;
31  import net.sourceforge.htmlunit.corejs.javascript.NativeFunction;
32  
33  import org.apache.commons.lang3.StringUtils;
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  import org.apache.xml.utils.PrefixResolver;
37  
38  import com.gargoylesoftware.htmlunit.BrowserVersion;
39  import com.gargoylesoftware.htmlunit.BrowserVersionFeatures;
40  import com.gargoylesoftware.htmlunit.ElementNotFoundException;
41  import com.gargoylesoftware.htmlunit.SgmlPage;
42  import com.gargoylesoftware.htmlunit.html.BaseFrameElement;
43  import com.gargoylesoftware.htmlunit.html.DomComment;
44  import com.gargoylesoftware.htmlunit.html.DomDocumentFragment;
45  import com.gargoylesoftware.htmlunit.html.DomElement;
46  import com.gargoylesoftware.htmlunit.html.DomNode;
47  import com.gargoylesoftware.htmlunit.html.DomText;
48  import com.gargoylesoftware.htmlunit.html.FrameWindow;
49  import com.gargoylesoftware.htmlunit.html.HTMLParser;
50  import com.gargoylesoftware.htmlunit.html.HtmlInput;
51  import com.gargoylesoftware.htmlunit.html.HtmlPage;
52  import com.gargoylesoftware.htmlunit.html.impl.SimpleRange;
53  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
54  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
55  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
56  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
57  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
58  import com.gargoylesoftware.htmlunit.javascript.configuration.WebBrowser;
59  import com.gargoylesoftware.htmlunit.javascript.host.dom.DOMImplementation;
60  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLCollection;
61  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
62  import com.gargoylesoftware.htmlunit.xml.XmlUtil;
63  
64  /**
65   * A JavaScript object for a Document.
66   *
67   * @version $Revision: 9081 $
68   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
69   * @author David K. Taylor
70   * @author <a href="mailto:chen_jun@users.sourceforge.net">Chen Jun</a>
71   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
72   * @author Chris Erskine
73   * @author Marc Guillemot
74   * @author Daniel Gredler
75   * @author Michael Ottati
76   * @author <a href="mailto:george@murnock.com">George Murnock</a>
77   * @author Ahmed Ashour
78   * @author Rob Di Marco
79   * @author Ronald Brill
80   * @author Chuck Dumont
81   * @author Frank Danek
82   * @see <a href="http://msdn.microsoft.com/en-us/library/ms531073.aspx">MSDN documentation</a>
83   * @see <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-html.html#ID-7068919">W3C Dom Level 1</a>
84   */
85  @JsxClass
86  public class Document extends EventNode {
87  
88      private static final Log LOG = LogFactory.getLog(Document.class);
89      private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\\w+");
90  
91      private Window window_;
92      private DOMImplementation implementation_;
93      private String designMode_;
94  
95      /**
96       * Sets the Window JavaScript object that encloses this document.
97       * @param window the Window JavaScript object that encloses this document
98       */
99      public void setWindow(final Window window) {
100         window_ = window;
101     }
102 
103     /**
104      * Returns the value of the "location" property.
105      * @return the value of the "location" property
106      */
107     @JsxGetter
108     public Location getLocation() {
109         return window_.getLocation();
110     }
111 
112     /**
113      * Sets the value of the "location" property. The location's default property is "href",
114      * so setting "document.location='http://www.sf.net'" is equivalent to setting
115      * "document.location.href='http://www.sf.net'".
116      * @see <a href="http://msdn.microsoft.com/en-us/library/ms535866.aspx">MSDN documentation</a>
117      * @param location the location to navigate to
118      * @throws IOException when location loading fails
119      */
120     @JsxSetter
121     public void setLocation(final String location) throws IOException {
122         window_.setLocation(location);
123     }
124 
125     /**
126      * Returns the value of the "referrer" property.
127      * @return the value of the "referrer" property
128      */
129     @JsxGetter
130     public String getReferrer() {
131         final String referrer = getPage().getWebResponse().getWebRequest().getAdditionalHeaders().get("Referer");
132         if (referrer == null) {
133             return "";
134         }
135         return referrer;
136     }
137 
138     /**
139      * Gets the JavaScript property "documentElement" for the document.
140      * @return the root node for the document
141      */
142     @JsxGetter
143     public Element getDocumentElement() {
144         final Object documentElement = getPage().getDocumentElement();
145         if (documentElement == null) {
146             // for instance with an XML document with parsing error
147             return null;
148         }
149         return (Element) getScriptableFor(documentElement);
150     }
151 
152     /**
153      * Gets the JavaScript property "doctype" for the document.
154      * @return the DocumentType of the document
155      */
156     @JsxGetter
157     public SimpleScriptable getDoctype() {
158         final Object documentType = getPage().getDoctype();
159         if (documentType == null) {
160             return null;
161         }
162         return getScriptableFor(documentType);
163     }
164 
165     /**
166      * Returns a value which indicates whether or not the document can be edited.
167      * @return a value which indicates whether or not the document can be edited
168      */
169     @JsxGetter
170     public String getDesignMode() {
171         if (designMode_ == null) {
172             if (getBrowserVersion().hasFeature(JS_DOCUMENT_DESIGN_MODE_INHERIT)) {
173                 designMode_ = "inherit";
174             }
175             else {
176                 designMode_ = "off";
177             }
178             if (getBrowserVersion().hasFeature(JS_DOCUMENT_DESIGN_MODE_CAPITAL_FIRST)) {
179                 designMode_ = StringUtils.capitalize(designMode_);
180             }
181         }
182         return designMode_;
183     }
184 
185     /**
186      * Sets a value which indicates whether or not the document can be edited.
187      * @param mode a value which indicates whether or not the document can be edited
188      */
189     @JsxSetter
190     public void setDesignMode(final String mode) {
191         final boolean inherit = getBrowserVersion().hasFeature(JS_DOCUMENT_DESIGN_MODE_INHERIT);
192         if (inherit) {
193             if (!"on".equalsIgnoreCase(mode) && !"off".equalsIgnoreCase(mode) && !"inherit".equalsIgnoreCase(mode)) {
194                 throw Context.reportRuntimeError("Invalid document.designMode value '" + mode + "'.");
195             }
196             if (!(getWindow().getWebWindow() instanceof FrameWindow)
197                 && getBrowserVersion().hasFeature(JS_DOCUMENT_DESIGN_MODE_ONLY_FOR_FRAMES)) {
198                 // IE evaluates all designMode changes for documents that aren't in frames as Off
199                 designMode_ = "off";
200             }
201             else if ("on".equalsIgnoreCase(mode)) {
202                 designMode_ = "on";
203             }
204             else if ("off".equalsIgnoreCase(mode)) {
205                 designMode_ = "off";
206             }
207             else if ("inherit".equalsIgnoreCase(mode)) {
208                 designMode_ = "inherit";
209             }
210 
211             if (getBrowserVersion().hasFeature(JS_DOCUMENT_DESIGN_MODE_CAPITAL_FIRST)) {
212                 designMode_ = StringUtils.capitalize(designMode_);
213             }
214         }
215         else {
216             if ("on".equalsIgnoreCase(mode)) {
217                 designMode_ = "on";
218                 final SgmlPage page = getPage();
219                 if (page != null && page.isHtmlPage()) {
220                     final HtmlPage htmlPage = (HtmlPage) page;
221                     final DomNode child = htmlPage.getBody().getFirstChild();
222                     final DomNode rangeNode = child == null ? htmlPage.getBody() : child;
223                     htmlPage.setSelectionRange(new SimpleRange(rangeNode, 0));
224                 }
225             }
226             else if ("off".equalsIgnoreCase(mode)) {
227                 designMode_ = "off";
228             }
229         }
230     }
231 
232     /**
233      * Returns the page that this document is modeling.
234      * @return the page that this document is modeling
235      */
236     protected SgmlPage getPage() {
237         return (SgmlPage) getDomNodeOrDie();
238     }
239 
240     /**
241      * Gets the window in which this document is contained.
242      * @return the window
243      */
244     @JsxGetter({ @WebBrowser(CHROME), @WebBrowser(FF), @WebBrowser(value = IE, minVersion = 11) })
245     public Object getDefaultView() {
246         return getWindow();
247     }
248 
249     /**
250      * Creates a new document fragment.
251      * @return a newly created document fragment
252      */
253     @JsxFunction
254     public Object createDocumentFragment() {
255         final DomDocumentFragment fragment = getDomNodeOrDie().getPage().createDomDocumentFragment();
256         final DocumentFragment node = new DocumentFragment();
257         node.setParentScope(getParentScope());
258         node.setPrototype(getPrototype(node.getClass()));
259         node.setDomNode(fragment);
260         return getScriptableFor(fragment);
261     }
262 
263     /**
264      * Creates a new HTML attribute with the specified name.
265      *
266      * @param attributeName the name of the attribute to create
267      * @return an attribute with the specified name
268      */
269     @JsxFunction
270     public Attr createAttribute(final String attributeName) {
271         return (Attr) getPage().createAttribute(attributeName).getScriptObject();
272     }
273 
274     /**
275      * Returns the {@link BoxObject} for the specific element.
276      *
277      * @param element target for BoxObject
278      * @return the BoxObject
279      */
280     public BoxObject getBoxObjectFor(final HTMLElement element) {
281         return element.getBoxObject();
282     }
283 
284     /**
285      * Imports a node from another document to this document.
286      * The source node is not altered or removed from the original document;
287      * this method creates a new copy of the source node.
288      *
289      * @param importedNode the node to import
290      * @param deep Whether to recursively import the subtree under the specified node; or not
291      * @return the imported node that belongs to this Document
292      */
293     @JsxFunction({ @WebBrowser(FF), @WebBrowser(value = IE, minVersion = 11) })
294     public Object importNode(final Node importedNode, final boolean deep) {
295         DomNode domNode = importedNode.getDomNodeOrDie();
296         domNode = domNode.cloneNode(deep);
297         domNode.processImportNode(this);
298         for (final DomNode childNode : domNode.getDescendants()) {
299             childNode.processImportNode(this);
300         }
301         return domNode.getScriptObject();
302     }
303 
304     /**
305      * Returns the implementation object of the current document.
306      * @return implementation-specific object
307      */
308     @JsxGetter
309     public DOMImplementation getImplementation() {
310         if (implementation_ == null) {
311             implementation_ = new DOMImplementation();
312             implementation_.setParentScope(getWindow());
313             implementation_.setPrototype(getPrototype(implementation_.getClass()));
314         }
315         return implementation_;
316     }
317 
318     /**
319      * Does nothing special anymore... just like FF.
320      * @param type the type of events to capture
321      * @see Window#captureEvents(String)
322      */
323     @JsxFunction({ @WebBrowser(CHROME), @WebBrowser(FF), @WebBrowser(value = IE, minVersion = 11) })
324     public void captureEvents(final String type) {
325         // Empty.
326     }
327 
328     /**
329      * Adapts any DOM node to resolve namespaces so that an XPath expression can be easily
330      * evaluated relative to the context of the node where it appeared within the document.
331      * @param nodeResolver the node to be used as a context for namespace resolution
332      * @return an XPathNSResolver which resolves namespaces with respect to the definitions
333      *         in scope for a specified node
334      */
335     @JsxFunction(@WebBrowser(FF))
336     public XPathNSResolver createNSResolver(final Node nodeResolver) {
337         final XPathNSResolver resolver = new XPathNSResolver();
338         resolver.setElement(nodeResolver);
339         resolver.setParentScope(getWindow());
340         resolver.setPrototype(getPrototype(resolver.getClass()));
341         return resolver;
342     }
343 
344     /**
345      * Create a new DOM text node with the given data.
346      *
347      * @param newData the string value for the text node
348      * @return the new text node or NOT_FOUND if there is an error
349      */
350     @JsxFunction
351     public Object createTextNode(final String newData) {
352         Object result = NOT_FOUND;
353         try {
354             final DomNode domNode = new DomText(getDomNodeOrDie().getPage(), newData);
355             final Object jsElement = getScriptableFor(domNode);
356 
357             if (jsElement == NOT_FOUND) {
358                 if (LOG.isDebugEnabled()) {
359                     LOG.debug("createTextNode(" + newData
360                             + ") cannot return a result as there isn't a JavaScript object for the DOM node "
361                             + domNode.getClass().getName());
362                 }
363             }
364             else {
365                 result = jsElement;
366             }
367         }
368         catch (final ElementNotFoundException e) {
369             // Just fall through - result is already set to NOT_FOUND
370         }
371         return result;
372     }
373 
374     /**
375      * Creates a new Comment.
376      * @param comment the comment text
377      * @return the new Comment
378      */
379     @JsxFunction
380     public Object createComment(final String comment) {
381         final DomNode domNode = new DomComment(getDomNodeOrDie().getPage(), comment);
382         return getScriptableFor(domNode);
383     }
384 
385     /**
386      * Evaluates an XPath expression string and returns a result of the specified type if possible.
387      * @param expression the XPath expression string to be parsed and evaluated
388      * @param contextNode the context node for the evaluation of this XPath expression
389      * @param resolver the resolver permits translation of all prefixes, including the XML namespace prefix,
390      *        within the XPath expression into appropriate namespace URIs.
391      * @param type If a specific type is specified, then the result will be returned as the corresponding type
392      * @param result the result object which may be reused and returned by this method
393      * @return the result of the evaluation of the XPath expression
394      */
395     @JsxFunction(@WebBrowser(FF))
396     public XPathResult evaluate(final String expression, final Node contextNode,
397             final Object resolver, final int type, final Object result) {
398         XPathResult xPathResult = (XPathResult) result;
399         if (xPathResult == null) {
400             xPathResult = new XPathResult();
401             xPathResult.setParentScope(getParentScope());
402             xPathResult.setPrototype(getPrototype(xPathResult.getClass()));
403         }
404 
405         PrefixResolver prefixResolver = null;
406         if (resolver instanceof NativeFunction) {
407             prefixResolver = new NativeFunctionPrefixResolver((NativeFunction) resolver, contextNode.getParentScope());
408         }
409         else if (resolver instanceof PrefixResolver) {
410             prefixResolver = (PrefixResolver) resolver;
411         }
412         xPathResult.init(contextNode.getDomNodeOrDie().getByXPath(expression, prefixResolver), type);
413         return xPathResult;
414     }
415 
416     /**
417      * Create a new HTML element with the given tag name.
418      *
419      * @param tagName the tag name
420      * @return the new HTML element, or NOT_FOUND if the tag is not supported
421      */
422     @JsxFunction
423     public Object createElement(String tagName) {
424         Object result = NOT_FOUND;
425         try {
426             final BrowserVersion browserVersion = getBrowserVersion();
427 
428             // FF3.6 supports document.createElement('div') or supports document.createElement('<div>')
429             // but not document.createElement('<div name="test">')
430             // IE9- supports also document.createElement('<div name="test">')
431             // FF4+ and IE11 don't support document.createElement('<div>')
432             if (browserVersion.hasFeature(BrowserVersionFeatures.JS_DOCUMENT_CREATE_ELEMENT_STRICT)
433                   && (tagName.contains("<") || tagName.contains(">"))) {
434                 LOG.info("createElement: Provided string '"
435                             + tagName + "' contains an invalid character; '<' and '>' are not allowed");
436                 throw Context.reportRuntimeError("String contains an invalid character");
437             }
438             else if (!browserVersion.hasFeature(JS_DOCUMENT_CREATE_ELEMENT_EXTENDED_SYNTAX)
439                   && tagName.startsWith("<") && tagName.endsWith(">")) {
440                 tagName = tagName.substring(1, tagName.length() - 1);
441 
442                 final Matcher matcher = TAG_NAME_PATTERN.matcher(tagName);
443                 if (!matcher.matches()) {
444                     LOG.info("createElement: Provided string '" + tagName + "' contains an invalid character");
445                     throw Context.reportRuntimeError("String contains an invalid character");
446                 }
447             }
448 
449             final SgmlPage page = getPage();
450             final org.w3c.dom.Element element = page.createElement(tagName);
451             if (element instanceof BaseFrameElement) {
452                 ((BaseFrameElement) element).markAsCreatedByJavascript();
453             }
454             else if (element instanceof HtmlInput) {
455                 ((HtmlInput) element).markAsCreatedByJavascript();
456             }
457             final Object jsElement = getScriptableFor(element);
458 
459             if (jsElement == NOT_FOUND) {
460                 if (LOG.isDebugEnabled()) {
461                     LOG.debug("createElement(" + tagName
462                         + ") cannot return a result as there isn't a JavaScript object for the element "
463                         + element.getClass().getName());
464                 }
465             }
466             else {
467                 result = jsElement;
468             }
469         }
470         catch (final ElementNotFoundException e) {
471             // Just fall through - result is already set to NOT_FOUND
472         }
473         return result;
474     }
475 
476     /**
477      * Creates a new HTML element with the given tag name, and name.
478      *
479      * @param namespaceURI the URI that identifies an XML namespace
480      * @param qualifiedName the qualified name of the element type to instantiate
481      * @return the new HTML element, or NOT_FOUND if the tag is not supported
482      */
483     @JsxFunction({ @WebBrowser(FF), @WebBrowser(CHROME), @WebBrowser(value = IE, minVersion = 11) })
484     public Object createElementNS(final String namespaceURI, final String qualifiedName) {
485         final org.w3c.dom.Element element;
486         if ("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul".equals(namespaceURI)) {
487             throw Context.reportRuntimeError("XUL not available");
488         }
489 
490         if (HTMLParser.XHTML_NAMESPACE.equals(namespaceURI)
491                 || HTMLParser.SVG_NAMESPACE.equals(namespaceURI)) {
492             element = getPage().createElementNS(namespaceURI, qualifiedName);
493         }
494         else {
495             element = new DomElement(namespaceURI, qualifiedName, getPage(), null);
496         }
497         return getScriptableFor(element);
498     }
499 
500     /**
501      * Returns all the descendant elements with the specified tag name.
502      * @param tagName the name to search for
503      * @return all the descendant elements with the specified tag name
504      */
505     @JsxFunction
506     public HTMLCollection getElementsByTagName(final String tagName) {
507         final String description = "Document.getElementsByTagName('" + tagName + "')";
508 
509         final HTMLCollection collection;
510         if ("*".equals(tagName)) {
511             collection = new HTMLCollection(getDomNodeOrDie(), false, description) {
512                 @Override
513                 protected boolean isMatching(final DomNode node) {
514                     return true;
515                 }
516             };
517         }
518         else {
519             final boolean useLocalName =
520                     getBrowserVersion().hasFeature(JS_GET_ELEMENTS_BY_TAG_NAME_NOT_SUPPORTS_NAMESPACES);
521 
522             collection = new HTMLCollection(getDomNodeOrDie(), false, description) {
523                 @Override
524                 protected boolean isMatching(final DomNode node) {
525                     if (useLocalName) {
526                         return tagName.equalsIgnoreCase(node.getLocalName());
527                     }
528                     return tagName.equalsIgnoreCase(node.getNodeName());
529                 }
530             };
531         }
532 
533         return collection;
534     }
535 
536     /**
537      * Returns a list of elements with the given tag name belonging to the given namespace.
538      * @param namespaceURI the namespace URI of elements to look for
539      * @param localName is either the local name of elements to look for or the special value "*",
540      *                  which matches all elements.
541      * @return a live NodeList of found elements in the order they appear in the tree
542      */
543     @JsxFunction({ @WebBrowser(FF), @WebBrowser(value = IE, minVersion = 11) })
544     public Object getElementsByTagNameNS(final Object namespaceURI, final String localName) {
545         final String description = "Document.getElementsByTagNameNS('" + namespaceURI + "', '" + localName + "')";
546 
547         final String prefix;
548         if (namespaceURI != null && !"*".equals(namespaceURI)) {
549             prefix = XmlUtil.lookupPrefix(getPage().getDocumentElement(), Context.toString(namespaceURI));
550         }
551         else {
552             prefix = null;
553         }
554 
555         final HTMLCollection collection = new HTMLCollection(getDomNodeOrDie(), false, description) {
556             @Override
557             protected boolean isMatching(final DomNode node) {
558                 if (!localName.equals(node.getLocalName())) {
559                     return false;
560                 }
561                 if (prefix == null) {
562                     return true;
563                 }
564                 return true;
565             }
566         };
567 
568         return collection;
569     }
570 }