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.activex.javascript.msxml;
16  
17  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
18  
19  import java.io.IOException;
20  import java.net.URL;
21  import java.util.HashMap;
22  import java.util.Map;
23  
24  import org.apache.commons.lang3.StringUtils;
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  import org.w3c.dom.CDATASection;
28  import org.w3c.dom.Node;
29  
30  import com.gargoylesoftware.htmlunit.ElementNotFoundException;
31  import com.gargoylesoftware.htmlunit.SgmlPage;
32  import com.gargoylesoftware.htmlunit.StringWebResponse;
33  import com.gargoylesoftware.htmlunit.WebRequest;
34  import com.gargoylesoftware.htmlunit.WebResponse;
35  import com.gargoylesoftware.htmlunit.WebWindow;
36  import com.gargoylesoftware.htmlunit.html.DomAttr;
37  import com.gargoylesoftware.htmlunit.html.DomComment;
38  import com.gargoylesoftware.htmlunit.html.DomDocumentFragment;
39  import com.gargoylesoftware.htmlunit.html.DomElement;
40  import com.gargoylesoftware.htmlunit.html.DomNode;
41  import com.gargoylesoftware.htmlunit.html.DomProcessingInstruction;
42  import com.gargoylesoftware.htmlunit.html.DomText;
43  import com.gargoylesoftware.htmlunit.html.HTMLParser;
44  import com.gargoylesoftware.htmlunit.html.HtmlElement;
45  import com.gargoylesoftware.htmlunit.html.HtmlPage;
46  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
47  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
48  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
49  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
50  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
51  import com.gargoylesoftware.htmlunit.xml.XmlPage;
52  
53  import net.sourceforge.htmlunit.corejs.javascript.Context;
54  
55  /**
56   * A JavaScript object for MSXML's (ActiveX) XMLDOMDocument.<br>
57   * Represents the top level of the XML source. Includes members for retrieving and creating all other XML objects.
58   * @see <a href="http://msdn.microsoft.com/en-us/library/ms756987.aspx">MSDN documentation</a>
59   *
60   * @author Ahmed Ashour
61   * @author Marc Guillemot
62   * @author Sudhan Moghe
63   * @author Ronald Brill
64   * @author Chuck Dumont
65   * @author Frank Danek
66   */
67  @JsxClass(IE)
68  public class XMLDOMDocument extends XMLDOMNode {
69  
70      private static final Log LOG = LogFactory.getLog(XMLDOMDocument.class);
71  
72      private boolean async_ = true;
73      private XMLDOMImplementation implementation_;
74      private boolean preserveWhiteSpace_;
75      private boolean preserveWhiteSpaceDuringLoad_ = true;
76      private XMLDOMParseError parseError_;
77      private Map<String, String> properties_ = new HashMap<>();
78      private String url_ = "";
79  
80      /**
81       * Creates a new instance.
82       */
83      public XMLDOMDocument() {
84          this(null);
85      }
86  
87      /**
88       * Creates a new instance, with associated {@link XmlPage}.
89       * @param enclosingWindow the window
90       */
91      public XMLDOMDocument(final WebWindow enclosingWindow) {
92          if (enclosingWindow != null) {
93              try {
94                  final XmlPage page = new XmlPage((WebResponse) null, enclosingWindow, true, false);
95                  setDomNode(page);
96              }
97              catch (final IOException e) {
98                  throw Context.reportRuntimeError("IOException: " + e);
99              }
100         }
101     }
102 
103     /**
104      * Returns if asynchronous download is permitted.
105      * @return if asynchronous download is permitted
106      */
107     @JsxGetter
108     public boolean isAsync() {
109         return async_;
110     }
111 
112     /**
113      * Sets if asynchronous download is permitted.
114      * @param async if asynchronous download is permitted
115      */
116     @JsxSetter
117     public void setAsync(final boolean async) {
118         async_ = async;
119     }
120 
121     /**
122      * Returns the document type node that specifies the DTD for this document.
123      * @return the document type node that specifies the DTD for this document
124      */
125     @JsxGetter
126     public XMLDOMDocumentType getDoctype() {
127         final Object documentType = getPage().getDoctype();
128         if (documentType == null) {
129             return null;
130         }
131         return (XMLDOMDocumentType) getScriptableFor(documentType);
132     }
133 
134     /**
135      * Returns the root element of the document.
136      * @return the root element of the document
137      */
138     @JsxGetter
139     public XMLDOMElement getDocumentElement() {
140         final Object documentElement = getPage().getDocumentElement();
141         if (documentElement == null) {
142             // for instance with an XML document with parsing error
143             return null;
144         }
145         return (XMLDOMElement) getScriptableFor(documentElement);
146     }
147 
148     /**
149      * Sets the root element of the document.
150      * @param element the root element of the document
151      */
152     @JsxSetter
153     public void setDocumentElement(final XMLDOMElement element) {
154         if (element == null) {
155             throw Context.reportRuntimeError("Type mismatch.");
156         }
157 
158         final XMLDOMElement documentElement = getDocumentElement();
159         if (documentElement != null) {
160             documentElement.getDomNodeOrDie().remove();
161         }
162 
163         appendChild(element);
164     }
165 
166     /**
167      * Returns the implementation object for the document.
168      * @return the implementation object for the document
169      */
170     @JsxGetter
171     public XMLDOMImplementation getImplementation() {
172         if (implementation_ == null) {
173             implementation_ = new XMLDOMImplementation();
174             implementation_.setParentScope(getWindow());
175             implementation_.setPrototype(getPrototype(implementation_.getClass()));
176         }
177         return implementation_;
178     }
179 
180     /**
181      * Attempting to set the value of documents generates an error.
182      * @param value the new value to set
183      */
184     @Override
185     public void setNodeValue(final String value) {
186         if (value == null || "null".equals(value)) {
187             throw Context.reportRuntimeError("Type mismatch.");
188         }
189         throw Context.reportRuntimeError("This operation cannot be performed with a node of type DOCUMENT.");
190     }
191 
192     /**
193      * {@inheritDoc}
194      */
195     @Override
196     public Object getOwnerDocument() {
197         return null;
198     }
199 
200     /**
201      * Returns a parse error object that contains information about the last parsing error.
202      * @return a parse error object
203      */
204     @JsxGetter
205     public XMLDOMParseError getParseError() {
206         if (parseError_ == null) {
207             parseError_ = new XMLDOMParseError();
208             parseError_.setParentScope(getParentScope());
209             parseError_.setPrototype(getPrototype(parseError_.getClass()));
210         }
211         return parseError_;
212     }
213 
214     /**
215      * Returns the default white space handling.
216      * @return the default white space handling
217      */
218     @JsxGetter
219     public boolean isPreserveWhiteSpace() {
220         return preserveWhiteSpace_;
221     }
222 
223     /**
224      * Set the default white space handling.
225      * @param preserveWhiteSpace the default white space handling
226      */
227     @JsxSetter
228     public void setPreserveWhiteSpace(final boolean preserveWhiteSpace) {
229         preserveWhiteSpace_ = preserveWhiteSpace;
230     }
231 
232     /**
233      * {@inheritDoc}
234      */
235     @Override
236     public Object getText() {
237         final XMLDOMElement element = getDocumentElement();
238         if (element == null) {
239             return "";
240         }
241         return element.getText();
242     }
243 
244     /**
245      * Attempting to set the text of documents generates an error.
246      * @param text the new text of this node
247      */
248     @Override
249     public void setText(final Object text) {
250         if (text == null || "null".equals(text)) {
251             throw Context.reportRuntimeError("Type mismatch.");
252         }
253         throw Context.reportRuntimeError("This operation cannot be performed with a node of type DOCUMENT.");
254     }
255 
256     /**
257      * Returns the URL for the last loaded XML document.
258      * @return the URL for the last loaded XML document
259      */
260     @JsxGetter
261     public String getUrl() {
262         return url_;
263     }
264 
265     /**
266      * Returns the XML representation of the node and all its descendants.
267      * @return an XML representation of this node and all its descendants
268      */
269     @Override
270     @JsxGetter
271     public String getXml() {
272         final XMLSerializer seralizer = new XMLSerializer(preserveWhiteSpaceDuringLoad_);
273         return seralizer.serializeToString(getDocumentElement());
274     }
275 
276     /**
277      * {@inheritDoc}
278      */
279     @Override
280     public Object appendChild(final Object newChild) {
281         verifyChild(newChild);
282 
283         return super.appendChild(newChild);
284     }
285 
286     private void verifyChild(final Object newChild) {
287         if (newChild == null || "null".equals(newChild) || !(newChild instanceof XMLDOMNode)) {
288             throw Context.reportRuntimeError("Type mismatch.");
289         }
290         if (newChild instanceof XMLDOMCDATASection) {
291             throw Context.reportRuntimeError("This operation cannot be performed with a node of type CDATA.");
292         }
293         if (newChild instanceof XMLDOMText) {
294             throw Context.reportRuntimeError("This operation cannot be performed with a node of type TEXT.");
295         }
296         if (newChild instanceof XMLDOMElement && getDocumentElement() != null) {
297             throw Context.reportRuntimeError("Only one top level element is allowed in an XML document.");
298         }
299         if (newChild instanceof XMLDOMDocumentFragment) {
300             boolean elementFound = false;
301             XMLDOMNode child = ((XMLDOMDocumentFragment) newChild).getFirstChild();
302             while (child != null) {
303                 if (child instanceof XMLDOMCDATASection) {
304                     throw Context.reportRuntimeError("This operation cannot be performed with a node of type CDATA.");
305                 }
306                 if (child instanceof XMLDOMText) {
307                     throw Context.reportRuntimeError("This operation cannot be performed with a node of type TEXT.");
308                 }
309                 if (child instanceof XMLDOMElement) {
310                     if (elementFound) {
311                         throw Context.reportRuntimeError("Only one top level element is allowed in an XML document.");
312                     }
313                     elementFound = true;
314                 }
315                 child = child.getNextSibling();
316             }
317         }
318     }
319 
320     /**
321      * Creates a new attribute with the specified name.
322      *
323      * @param name the name of the new attribute object
324      * @return the new attribute object
325      */
326     @JsxFunction
327     public Object createAttribute(final String name) {
328         if (name == null || "null".equals(name)) {
329             throw Context.reportRuntimeError("Type mismatch.");
330         }
331         if (StringUtils.isBlank(name) || name.indexOf('<') >= 0 || name.indexOf('>') >= 0) {
332             throw Context.reportRuntimeError("To create a node of type ATTR a valid name must be given.");
333         }
334 
335         final DomAttr domAttr = getPage().createAttribute(name);
336         return getScriptableFor(domAttr);
337     }
338 
339     /**
340      * Creates a CDATA section node that contains the supplied data.
341      * @param data the value to be supplied to the new CDATA section object's <code>nodeValue</code> property
342      * @return the new CDATA section object
343      */
344     @JsxFunction
345     public Object createCDATASection(final String data) {
346         final CDATASection domCDATASection = getPage().createCDATASection(data);
347         return getScriptableFor(domCDATASection);
348     }
349 
350     /**
351      * Creates a comment node that contains the supplied data.
352      * @param data the value to be supplied to the new comment object's <code>nodeValue</code> property
353      * @return the new comment object
354      */
355     @JsxFunction
356     public Object createComment(final String data) {
357         final DomComment domComment = new DomComment(getPage(), data);
358         return getScriptableFor(domComment);
359     }
360 
361     /**
362      * Creates an empty document fragment object.
363      * @return the new document fragment object
364      */
365     @JsxFunction
366     public Object createDocumentFragment() {
367         final DomDocumentFragment domDocumentFragment = new DomDocumentFragment(getPage());
368         return getScriptableFor(domDocumentFragment);
369     }
370 
371     /**
372      * Creates an element node using the specified name.
373      * @param tagName the name for the new element node
374      * @return the new element object or <code>NOT_FOUND</code> if the tag is not supported
375      */
376     @JsxFunction
377     public Object createElement(final String tagName) {
378         if (tagName == null || "null".equals(tagName)) {
379             throw Context.reportRuntimeError("Type mismatch.");
380         }
381         if (StringUtils.isBlank(tagName) || tagName.indexOf('<') >= 0 || tagName.indexOf('>') >= 0) {
382             throw Context.reportRuntimeError("To create a node of type ELEMENT a valid name must be given.");
383         }
384 
385         Object result = NOT_FOUND;
386         try {
387             final DomElement domElement = (DomElement) getPage().createElement(tagName);
388             final Object jsElement = getScriptableFor(domElement);
389 
390             if (jsElement == NOT_FOUND) {
391                 if (LOG.isDebugEnabled()) {
392                     LOG.debug("createElement(" + tagName
393                         + ") cannot return a result as there isn't a JavaScript object for the element "
394                         + domElement.getClass().getName());
395                 }
396             }
397             else {
398                 result = jsElement;
399             }
400         }
401         catch (final ElementNotFoundException e) {
402             // Just fall through - result is already set to NOT_FOUND
403         }
404         return result;
405     }
406 
407     /**
408      * Creates a node using the supplied type, name, and namespace.
409      * @param type a value that uniquely identifies the node type
410      * @param name the value for the new node's <code>nodeName</code> property
411      * @param namespaceURI the namespace URI.
412      *        If specified, the node is created in the context of the namespaceURI parameter
413      *        with the prefix specified on the node name.
414      *        If the name parameter does not have a prefix, this is treated as the default namespace.
415      * @return the newly created node
416      */
417     @JsxFunction
418     public Object createNode(final Object type, final String name, final Object namespaceURI) {
419         switch ((short) Context.toNumber(type)) {
420             case Node.ELEMENT_NODE:
421                 return createElementNS((String) namespaceURI, name);
422             case Node.ATTRIBUTE_NODE:
423                 return createAttribute(name);
424 
425             default:
426                 throw Context.reportRuntimeError("xmlDoc.createNode(): Unsupported type "
427                         + (short) Context.toNumber(type));
428         }
429     }
430 
431     /**
432      * Creates a new HTML element with the given tag name, and name.
433      * @param namespaceURI the URI that identifies an XML namespace
434      * @param qualifiedName the qualified name of the element type to instantiate
435      * @return the new element or NOT_FOUND if the tag is not supported
436      */
437     private Object createElementNS(final String namespaceURI, final String qualifiedName) {
438         final org.w3c.dom.Element element;
439         if ("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul".equals(namespaceURI)) {
440             throw Context.reportRuntimeError("XUL not available");
441         }
442         else if (HTMLParser.XHTML_NAMESPACE.equals(namespaceURI)
443                 || HTMLParser.SVG_NAMESPACE.equals(namespaceURI)) {
444             element = getPage().createElementNS(namespaceURI, qualifiedName);
445         }
446         else {
447             element = new DomElement(namespaceURI, qualifiedName, getPage(), null);
448         }
449         return getScriptableFor(element);
450     }
451 
452     /**
453      * Creates a processing instruction node that contains the supplied target and data.
454      * @param target the target part of the processing instruction
455      * @param data the rest of the processing instruction preceding the closing ?&gt; characters
456      * @return the new processing instruction object
457      */
458     @JsxFunction
459     public Object createProcessingInstruction(final String target, final String data) {
460         final DomProcessingInstruction domProcessingInstruction =
461                 ((XmlPage) getPage()).createProcessingInstruction(target, data);
462         return getScriptableFor(domProcessingInstruction);
463     }
464 
465     /**
466      * Creates a text node that contains the supplied data.
467      * @param data the value to be supplied to the new text object's <code>nodeValue</code> property
468      * @return the new text object or <code>NOT_FOUND</code> if there is an error
469      */
470     @JsxFunction
471     public Object createTextNode(final String data) {
472         Object result = NOT_FOUND;
473         try {
474             final DomText domText = new DomText(getPage(), data);
475             final Object jsElement = getScriptableFor(domText);
476 
477             if (jsElement == NOT_FOUND) {
478                 if (LOG.isDebugEnabled()) {
479                     LOG.debug("createTextNode(" + data
480                             + ") cannot return a result as there isn't a JavaScript object for the DOM node "
481                             + domText.getClass().getName());
482                 }
483             }
484             else {
485                 result = jsElement;
486             }
487         }
488         catch (final ElementNotFoundException e) {
489             // Just fall through - result is already set to NOT_FOUND
490         }
491         return result;
492     }
493 
494     /**
495      * Returns a collection of elements that have the specified name.
496      * @param tagName the element name to find; the <code>tagName</code> value '*' returns all elements in the document
497      * @return a collection of elements that match the specified name
498      */
499     @JsxFunction
500     public XMLDOMNodeList getElementsByTagName(final String tagName) {
501         final DomNode firstChild = getDomNodeOrDie().getFirstChild();
502         if (firstChild == null) {
503             return XMLDOMNodeList.emptyCollection(this);
504         }
505 
506         final XMLDOMNodeList collection = new XMLDOMNodeList(getDomNodeOrDie(), false,
507                 "XMLDOMDocument.getElementsByTagName") {
508             @Override
509             protected boolean isMatching(final DomNode node) {
510                 return node.getNodeName().equals(tagName);
511             }
512         };
513 
514         return collection;
515     }
516 
517     /**
518      * Retrieves the value of one of the second-level properties that are set either by default or using the
519      * {@link #setProperty(String, String)} method.
520      * @param name the name of the property
521      * @return the property value
522      */
523     @JsxFunction
524     public String getProperty(final String name) {
525         return properties_.get(name);
526     }
527 
528     /**
529      * {@inheritDoc}
530      */
531     @Override
532     protected Object insertBeforeImpl(final Object[] args) {
533         final Object newChild = args[0];
534         verifyChild(newChild);
535         if (args.length != 2) {
536             throw Context.reportRuntimeError("Wrong number of arguments or invalid property assignment.");
537         }
538 
539         return super.insertBeforeImpl(args);
540     }
541 
542     /**
543      * Loads an XML document from the specified location.
544      * @param xmlSource a URL that specifies the location of the XML file
545      * @return {@code true} if the load succeeded; {@code false} if the load failed
546      */
547     @JsxFunction
548     public boolean load(final String xmlSource) {
549         if (async_ && LOG.isDebugEnabled()) {
550             LOG.debug("XMLDOMDocument.load(): 'async' is true, currently treated as false.");
551         }
552         try {
553             final HtmlPage htmlPage = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
554             final URL fullyQualifiedURL = htmlPage.getFullyQualifiedUrl(xmlSource);
555             final WebRequest request = new WebRequest(fullyQualifiedURL);
556             final WebResponse webResponse = getWindow().getWebWindow().getWebClient().loadWebResponse(request);
557             final XmlPage page = new XmlPage(webResponse, getWindow().getWebWindow(), false, false);
558             setDomNode(page);
559 
560             preserveWhiteSpaceDuringLoad_ = preserveWhiteSpace_;
561             url_ = fullyQualifiedURL.toExternalForm();
562             return true;
563         }
564         catch (final IOException e) {
565             final XMLDOMParseError parseError = getParseError();
566             parseError.setErrorCode(-1);
567             parseError.setFilepos(1);
568             parseError.setLine(1);
569             parseError.setLinepos(1);
570             parseError.setReason(e.getMessage());
571             parseError.setSrcText("xml");
572             parseError.setUrl(xmlSource);
573             if (LOG.isDebugEnabled()) {
574                 LOG.debug("Error parsing XML from '" + xmlSource + "'", e);
575             }
576             return false;
577         }
578     }
579 
580     /**
581      * Loads an XML document using the supplied string.
582      * @param strXML the XML string to load into this XML document object;
583      *     this string can contain an entire XML document or a well-formed fragment
584      * @return {@code true} if the load succeeded; {@code false} if the load failed
585      */
586     @JsxFunction
587     public boolean loadXML(final String strXML) {
588         try {
589             final WebWindow webWindow = getWindow().getWebWindow();
590 
591             final WebResponse webResponse = new StringWebResponse(strXML, webWindow.getEnclosedPage().getUrl());
592             final XmlPage page = new XmlPage(webResponse, webWindow, false, false);
593             setDomNode(page);
594 
595             preserveWhiteSpaceDuringLoad_ = preserveWhiteSpace_;
596             url_ = "";
597             return true;
598         }
599         catch (final IOException e) {
600             final XMLDOMParseError parseError = getParseError();
601             parseError.setErrorCode(-1);
602             parseError.setFilepos(1);
603             parseError.setLine(1);
604             parseError.setLinepos(1);
605             parseError.setReason(e.getMessage());
606             parseError.setSrcText("xml");
607             parseError.setUrl("");
608             if (LOG.isDebugEnabled()) {
609                 LOG.debug("Error parsing XML\n" + strXML, e);
610             }
611             return false;
612         }
613     }
614 
615     /**
616      * Returns the node that matches the ID attribute.
617      * @param id the value of the ID to match
618      * @return since we are not processing DTD, this method always returns {@code null}
619      */
620     @JsxFunction
621     public Object nodeFromID(final String id) {
622         return null;
623     }
624 
625     /**
626      * This method is used to set second-level properties on the DOM object.
627      * @param name the name of the property to be set
628      * @param value the value of the specified property
629      */
630     @JsxFunction
631     public void setProperty(final String name, final String value) {
632         properties_.put(name, value);
633     }
634 
635     /**
636      * @return the preserveWhiteSpaceDuringLoad
637      */
638     public boolean isPreserveWhiteSpaceDuringLoad() {
639         return preserveWhiteSpaceDuringLoad_;
640     }
641 
642     /**
643      * @return the page that this document is modeling
644      */
645     protected SgmlPage getPage() {
646         return (SgmlPage) getDomNodeOrDie();
647     }
648 
649     /**
650      * {@inheritDoc}
651      */
652     @Override
653     public MSXMLScriptable makeScriptableFor(final DomNode domNode) {
654         final MSXMLScriptable scriptable;
655 
656         if (domNode instanceof DomElement && !(domNode instanceof HtmlElement)) {
657             scriptable = new XMLDOMElement();
658         }
659         else if (domNode instanceof DomAttr) {
660             scriptable = new XMLDOMAttribute();
661         }
662         else {
663             return (MSXMLScriptable) super.makeScriptableFor(domNode);
664         }
665 
666         scriptable.setParentScope(this);
667         scriptable.setPrototype(getPrototype(scriptable.getClass()));
668         scriptable.setDomNode(domNode);
669 
670         return scriptable;
671     }
672 
673     /**
674      * {@inheritDoc}
675      */
676     @Override
677     protected void initParentScope(final DomNode domNode, final SimpleScriptable scriptable) {
678         scriptable.setParentScope(getParentScope());
679     }
680 }