1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2009, 2010, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2005, 2006, 2007, 2009, 2010, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 
 25 /**
 26  * Default constructor.
 27  * @class
 28  * Do not directly instantiate {@link AjxXmlDoc}, use one of the create factory methods instead.
 29  * 
 30  */
 31 AjxXmlDoc = function() {
 32 	if (!AjxXmlDoc._inited)
 33 		AjxXmlDoc._init();
 34 }
 35 
 36 AjxXmlDoc.prototype.isAjxXmlDoc = true;
 37 AjxXmlDoc.prototype.toString = function() {	return "AjxXmlDoc"; }
 38 
 39 //
 40 // Constants
 41 //
 42 
 43 /**
 44  * <strong>Note:</strong>
 45  * Anybody that uses these regular expressions MUST reset the <code>lastIndex</code>
 46  * property to zero or else the results are not guaranteed to be correct. You should
 47  * use {@link AjxXmlDoc.replaceInvalidChars} instead.
 48  * 
 49  * @private
 50  */
 51 AjxXmlDoc.INVALID_CHARS_RE = /[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE-\uFFFF]/g;
 52 AjxXmlDoc.REC_AVOID_CHARS_RE = /[\u007F-\u0084\u0086-\u009F\uFDD0-\uFDDF]/g;
 53 
 54 //
 55 // Data
 56 //
 57 
 58 AjxXmlDoc._inited = false;
 59 AjxXmlDoc._msxmlVers = null;
 60 AjxXmlDoc._useDOM =
 61     Boolean(document.implementation && document.implementation.createDocument);
 62 AjxXmlDoc._useActiveX =
 63     !AjxXmlDoc._useDOM && Boolean(window.ActiveXObject);
 64 
 65 /**
 66  * Creates an XML doc.
 67  * 
 68  * @return	{AjxXmlDoc}	the XML doc
 69  */
 70 AjxXmlDoc.create =
 71 function() {
 72 	var xmlDoc = new AjxXmlDoc();
 73 	var newDoc = null;
 74 	if (AjxXmlDoc._useActiveX) {
 75 		newDoc = new ActiveXObject(AjxXmlDoc._msxmlVers);
 76 		newDoc.async = true; // Force Async loading
 77 		if (AjxXmlDoc._msxmlVers == "MSXML2.DOMDocument.4.0") {
 78 			newDoc.setProperty("SelectionLanguage", "XPath");
 79 			newDoc.setProperty("SelectionNamespaces", "xmlns:zimbra='urn:zimbra' xmlns:mail='urn:zimbraMail' xmlns:account='urn:zimbraAccount'");
 80 		}
 81 	} else if (AjxXmlDoc._useDOM) {
 82 		newDoc = document.implementation.createDocument("", "", null);
 83 	} else {
 84 		throw new AjxException("Unable to create new Doc", AjxException.INTERNAL_ERROR, "AjxXmlDoc.create");
 85 	}
 86 	xmlDoc._doc = newDoc;
 87 	return xmlDoc;
 88 }
 89 
 90 /**
 91  * Creates an XML doc from a document object.
 92  * 
 93  * @param	{Document}		doc		the document object
 94  * @return	{AjxXmlDoc}		the XML doc
 95  */
 96 AjxXmlDoc.createFromDom =
 97 function(doc) {
 98 	var xmlDoc = new AjxXmlDoc();
 99 	xmlDoc._doc = doc;
100 	return xmlDoc;
101 }
102 
103 /**
104  * Creates an XML doc from an XML string.
105  * 
106  * @param	{string}		xml		the XML string
107  * @return	{AjxXmlDoc}		the XML doc
108  */
109 AjxXmlDoc.createFromXml =
110 function(xml) {
111 	var xmlDoc = AjxXmlDoc.create();
112 	xmlDoc.loadFromString(xml);
113 	return xmlDoc;
114 }
115 
116 /**
117  * Replaces invalid characters in the given string.
118  * 
119  * @param	{string}	s	the string
120  * @return	{string}	the resulting string
121  */
122 AjxXmlDoc.replaceInvalidChars = function(s) {
123 	AjxXmlDoc.INVALID_CHARS_RE.lastIndex = 0;
124 	return s.replace(AjxXmlDoc.INVALID_CHARS_RE, "?");
125 };
126 
127 AjxXmlDoc.getXml = function(node) {
128 
129     if (!node) {
130         return '';
131     }
132 
133     var xml = node.xml;
134     if (!xml) {
135         var ser = new XMLSerializer();
136         xml = ser.serializeToString(node);
137     }
138 
139     return AjxXmlDoc.replaceInvalidChars(xml);
140 };
141 
142 /**
143  * Gets the document.
144  * 
145  * @return	{Document}	the document
146  */
147 AjxXmlDoc.prototype.getDoc =
148 function() {
149 	return this._doc;
150 }
151 
152 AjxXmlDoc.prototype.loadFromString =
153 function(str) {
154 	var doc = this._doc;
155 	doc.loadXML(str);
156 	if (AjxXmlDoc._useActiveX) {
157 		if (doc.parseError.errorCode != 0)
158 			throw new AjxException(doc.parseError.reason, AjxException.INVALID_PARAM, "AjxXmlDoc.loadFromString");
159 	}
160 }
161 
162 AjxXmlDoc.prototype.loadFromUrl =
163 function(url) {
164 	if(AjxEnv.isChrome || AjxEnv.isSafari) {
165 		var xmlhttp = new window.XMLHttpRequest();
166 		xmlhttp.open("GET", url, false);
167 		xmlhttp.send(null);
168 		var xmlDoc = xmlhttp.responseXML;
169 		this._doc = xmlDoc;
170 	} else {
171 		this._doc.load(url);
172 	}
173 }
174 
175 /**
176  * This function tries to create a JavaScript representation of the DOM. In some cases,
177  * it is easier to work with JS objects rather than do DOM lookups.
178  *
179  * <p>
180  * Rules:
181  * <ol>
182  * <li>The top-level tag gets lost; only it's content is seen important.</li>
183  * <li>Each node will be represented as a JS object.  It's textual content
184  *      will be saved in node.__msh_content (returned by <code>toString()</code>).</li>
185  * <li>Attributes get discarded.</li>
186  * <li>Each subnode will map to a property with its tagName in the parent
187  *      node <code>parent[subnode.tagName] == subnode</code></li>
188  * <li>If multiple nodes with the same tagName have the same parent node, then
189  *      <code>parent[tagName]</code> will be an array containing the objects, rather than a
190  *      single object.</li>
191  * </ol>
192  * 
193  * So what this function allows us to do is for instance this, starting with this XML doc:
194  *
195  * <pre>
196  * <error>
197  *   <code>404</code>
198  *   <name>Not Found</name>
199  *   <description>Page wasn't found on this server.</description>
200  * </error>
201  * </pre>
202  * 
203  * <pre>
204  * var obj = AjxXmlDoc.createFromXml(XML).toJSObject();
205  * alert(obj.code + " " + obj.name + " " + obj.description);
206  * </pre>
207  * 
208  * Here's an array example:
209  * <pre>
210  * <return>
211  *   <item>
212  *     <name>John Doe</name>
213  *     <email>foo@bar.com</email>
214  *   </item>
215  *   <item>
216  *     <name>Johnny Bravo</name>
217  *     <email>bravo@cartoonnetwork.com</email>
218  *   </item>
219  * </return>
220  * </pre>
221  * 
222  * <pre>
223  * var obj = AjxXmlDoc.createFromXml(XML).toJSObject();
224  * for (var i = 0; i < obj.item.length; ++i) {
225  *   alert(obj.item[i].name + " / " + obj.item[i].email);
226  * }
227  * </pre>
228  *
229  * Note that if there's only one <item> tag, then obj.item will be an object
230  * rather than an array.  And if there is no <item> tag, then obj.item will be
231  * undefined.  These are cases that the calling application must take care of.
232  */
233 AjxXmlDoc.prototype.toJSObject = 
234 function(dropns, lowercase, withAttrs) {
235 	_node = function() { this.__msh_content = ''; };
236 	_node.prototype.toString = function() { return this.__msh_content; };
237 	rec = function(i, o) {
238 		var tags = {}, t, n;
239 		for (i = i.firstChild; i; i = i.nextSibling) {
240 			if (i.nodeType == 1) {
241 				t = i.tagName;
242 				if (dropns)      t = t.replace(/^.*?:/, "");
243 				if (lowercase)   t = t.toLowerCase();
244 				n = new _node();
245 				if (tags[t]) {
246 					if (tags[t] == 1) {
247 						o[t] = [ o[t] ];
248 						tags[t] = 2;
249 					}
250 					o[t].push(n);
251 				} else {
252 					o[t] = n;
253 					tags[t] = 1;
254 				}
255 				//do attributes
256 				if(withAttrs) {
257 					if(i.attributes && i.attributes.length) {
258 						for(var ix = 0;ix<i.attributes.length;ix++) {
259 							attr = i.attributes[ix];
260 							n[attr.name] = AjxUtil.isNumeric(attr.value) ? attr.value : String(attr.value);
261 						}
262 					}
263 				}
264 				rec(i, n);
265 			} else if (i.nodeType == 3)
266 				o.__msh_content += i.nodeValue;
267 		}
268 	};
269 	var o = new _node();
270 	rec(this._doc.documentElement, o);
271 	return o;
272 };
273 
274 AjxXmlDoc.prototype.getElementsByTagNameNS = 
275 function(ns, tag) {
276 	var doc = this.getDoc();
277 	return !doc.getElementsByTagNameNS
278 		? doc.getElementsByTagName(ns + ":" + tag)
279 		: doc.getElementsByTagNameNS(ns, tag);
280 };
281 
282 AjxXmlDoc.prototype.getFirstElementByTagNameNS = 
283 function(ns, tag) {
284 	return this.getElementsByTagNameNS(ns, tag)[0];
285 };
286 
287 AjxXmlDoc.prototype.getElementsByTagName = 
288 function(tag) {
289 	var doc = this.getDoc();
290 	return doc.getElementsByTagName(tag);
291 };
292 
293 AjxXmlDoc._init =
294 function() {
295 	if (AjxXmlDoc._useActiveX) {
296 		var msxmlVers = ["MSXML4.DOMDocument", "MSXML3.DOMDocument", "MSXML2.DOMDocument.4.0",
297 				 "MSXML2.DOMDocument.3.0", "MSXML2.DOMDocument", "MSXML.DOMDocument",
298 				 "Microsoft.XmlDom"];
299 		for (var i = 0; i < msxmlVers.length; i++) {
300 			try {
301 				new ActiveXObject(msxmlVers[i]);
302 				AjxXmlDoc._msxmlVers = msxmlVers[i];
303 				break;
304 			} catch (ex) {
305 			}
306 		}
307 		if (!AjxXmlDoc._msxmlVers) {
308 			throw new AjxException("MSXML not installed", AjxException.INTERNAL_ERROR, "AjxXmlDoc._init");
309 		}
310 	} else if (!Document.prototype.loadXML) {
311 		// add loadXML to Document's API
312 		Document.prototype.loadXML = function(str) {
313 			var domParser = new DOMParser();
314 			var domObj = domParser.parseFromString(str, "text/xml");
315 			// remove old child nodes since we recycle DOMParser and append new
316 			while (this.hasChildNodes()) {
317 				this.removeChild(this.lastChild);
318 			}
319 			var len = domObj.childNodes.length;
320             for (var i = 0; i < len; i++) {
321                 var importedNode = this.importNode(domObj.childNodes[i], true);
322                 this.appendChild(importedNode);
323             }
324 		}
325 		
326 		if (AjxEnv.isNav) {
327 			_NodeGetXml = function() {
328 				var ser = new XMLSerializer();
329 				return ser.serializeToString(this);
330 			}
331 			Node.prototype.__defineGetter__("xml", _NodeGetXml);
332 		}
333 	}
334 	
335 	AjxXmlDoc._inited = true;
336 };
337 
338 AjxXmlDoc.prototype.set =
339 function(name, value, element) {
340    var p = this._doc.createElement(name);
341       if (value != null) {
342          var cdata = this._doc.createTextNode("");
343          p.appendChild(cdata);
344          cdata.nodeValue = value;
345       }
346       if (element == null) {
347          this.root.appendChild(p);
348       } else {
349          element.appendChild(p);
350       }
351    return p;
352 };
353 
354 AjxXmlDoc.prototype.getXml = function() {
355     return AjxXmlDoc.getXml(this.getDoc());
356 };
357 AjxXmlDoc.prototype.getDocXml = AjxXmlDoc.prototype.getXml;     // back-compatibility with old name
358 
359 /**
360  * Creates an XML document with a root element.
361  * 
362  * @param	{string}	rootName	the root name
363  * @return	{AjxXmlDoc}	the XML document
364  */
365 AjxXmlDoc.createRoot =
366 function(rootName) {
367    var xmldoc = AjxXmlDoc.create();
368    var d = xmldoc.getDoc();
369    xmldoc.root = d.createElement(rootName);
370 
371    d.appendChild(xmldoc.root);
372    return xmldoc;
373 };
374 
375 /**
376  * Creates an XML document with the element.
377  * 
378  * @param	{string}	name	the element name
379  * @param	{string}	value	the element value
380  * @return	{AjxXmlDoc}	the XML document
381  */
382 AjxXmlDoc.createElement =
383 function(name, value) {
384 	
385    var xmldoc = AjxXmlDoc.create();
386    var d = xmldoc.getDoc();
387    xmldoc.root = d.createElement(name);
388    if (value != null) {
389    		//xmldoc.root.nodeValue = value;
390    	 	var cdata = d.createTextNode("");
391         xmldoc.root.appendChild(cdata);
392         cdata.nodeValue = value;
393    }
394    
395    d.appendChild(xmldoc.root);
396    return xmldoc;
397    
398 };
399 
400 
401 AjxXmlDoc.prototype.appendChild =
402 function(xmldoc){
403    //Security Exception WRONG_DOCUMENT_ERR thrown when we append nodes created of diff. documents
404    //Chrome/Safari does not like it.
405    if(this._doc != xmldoc._doc && ( AjxEnv.isChrome || AjxEnv.isSafari )){
406         this.root.appendChild(this.getDoc().importNode(xmldoc.root, true));
407    }else{
408         this.root.appendChild(xmldoc.root);
409    }
410 };
411 
412