/* sogoWebDAV.js - This file is part of "SOGo Connector", a Thunderbird extension. * * Copyright: Inverse inc., 2006-2014 * * "SOGo Connector" is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 as published by * the Free Software Foundation; * * "SOGo Connector" is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * "SOGo Connector"; if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); Components.utils.import("resource://calendar/modules/calProviderUtils.jsm"); function jsInclude(files, target) { let loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"] .getService(Components.interfaces.mozIJSSubScriptLoader); for (let i = 0; i < files.length; i++) { try { loader.loadSubScript(files[i], target); } catch(e) { dump("sogoWebDAV.js: failed to include '" + files[i] + "'\n" + e + "\nFile: " + e.fileName + "\nLine: " + e.lineNumber + "\n\n Stack:\n\n" + e.stack); } } } jsInclude(["chrome://inverse-library/content/uuid.js"]); function backtrace(aDepth) { let depth = aDepth || 10; let stack = ""; let frame = arguments.callee.caller; for (let i = 1; i <= depth && frame; i++) { stack += i+": "+ frame.name + "\n"; frame = frame.caller; } return stack; } function XMLToJSONParser(doc) { this._buildTree(doc); } XMLToJSONParser.prototype = { _buildTree: function XMLToJSONParser_buildTree(doc) { let nodeName = doc.documentElement.localName; this[nodeName] = [this._translateNode(doc.documentElement)]; // dump("Parsed XMLToJSON object: " + dumpObject(this) + "\n"); }, _translateNode: function XMLToJSONParser_translateNode(node) { let value = null; if (node.childNodes.length) { let textValue = ""; let dictValue = {}; let hasElements = false; for (let i = 0; i < node.childNodes.length; i++) { let currentNode = node.childNodes[i]; let nodeName = currentNode.localName; if (currentNode.nodeType == Components.interfaces.nsIDOMNode.TEXT_NODE) { textValue += currentNode.nodeValue; } else if (currentNode.nodeType == Components.interfaces.nsIDOMNode.ELEMENT_NODE) { hasElements = true; let nodeValue = this._translateNode(currentNode); if (!dictValue[nodeName]) dictValue[nodeName] = []; dictValue[nodeName].push(nodeValue); } } if (hasElements) value = dictValue; else value = textValue; } return value; } }; function xmlEscape(text) { let s = ""; for (var i = 0; i < text.length; i++) { if (text[i] == "&") { s += "&"; } else if (text[i] == "<") { s += "<"; } else { let charCode = text.charCodeAt(i); if (charCode > 127) { s += '&#' + charCode + ';'; } else { s += text[i]; } } } return s; } function xmlUnescape(text) { let s = (""+text).replace(/</g, "<", "g"); s = s.replace(/>/g, ">", "g"); s = s.replace(/&/g, "&", "g"); return s; } function sogoWebDAV(url, target, data, synchronous, asJSON) { this.url = url; this.target = target; this.cbData = data; if (typeof synchronous == "undefined") { this.synchronous = false; } else { this.synchronous = synchronous; } this.requestJSONResponse = false; this.requestXMLResponse = false; if (typeof asJSON != "undefined") { this.requestJSONResponse = asJSON; this.requestXMLResponse = !asJSON; } } sogoWebDAV.prototype = { QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIInterfaceRequestor]), _makeURI: function _makeURI(url) { var ioSvc = Components.classes["@mozilla.org/network/io-service;1"]. getService(Components.interfaces.nsIIOService); return ioSvc.newURI(url, null, null); }, // See: http://mxr.mozilla.org/comm-central/source/calendar/base/modules/calProviderUtils.jsm getInterface: function sogoWebDAV_getInterface(aIID) { if (aIID.equals(Components.interfaces.nsIProgressEventSink)) { return { onProgress: function sogoWebDAV_onProgress(aRequest, aContext, aProgress, aProgressMax) {}, onStatus: function sogoWebDAV_onStatus(aRequest, aContext, aStatus, aStatusArg) {} }; } return cal.InterfaceRequestor_getInterface.apply(this, arguments); }, _sendHTTPRequest: function(method, body, headers) { let IOService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService2); let channel = IOService.newChannelFromURI(this._makeURI(this.url)); let httpChannel = channel.QueryInterface(Components.interfaces.nsIHttpChannel); httpChannel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; httpChannel.notificationCallbacks = this; if (headers && headers.accept) { httpChannel.setRequestHeader("accept", headers.accept, false); delete headers.accept; } else { httpChannel.setRequestHeader("accept", "text/xml", false); } httpChannel.setRequestHeader("accept-charset", "utf-8,*;q=0.1", false); if (headers) { for (let header in headers) { httpChannel.setRequestHeader(header, headers[header], true); } } if (body) { httpChannel = httpChannel.QueryInterface(Components.interfaces.nsIUploadChannel); let converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; let stream = converter.convertToInputStream(body); let contentType = headers["content-type"]; if (!contentType) { contentType = "text/plain; charset=utf-8"; } httpChannel.setUploadStream(stream, contentType, -1); } /* If set too early, the method can change to "PUT" when initially set to "PROPFIND"... */ httpChannel.requestMethod = method; if (this.synchronous) { let inStream = httpChannel.open(); let byteStream = Components.classes["@mozilla.org/binaryinputstream;1"] .createInstance(Components.interfaces.nsIBinaryInputStream); byteStream.setInputStream(inStream); let resultLength = 0; let result = ""; let le; while ((le = inStream.available())) { resultLength += le; result += byteStream.readBytes(le); } this._handleHTTPResponse(httpChannel, resultLength, result); } else { let this_ = this; let listener = { onStreamComplete: function(aLoader, aContext, aStatus, aResultLength, aResult) { this_._handleHTTPResponse(httpChannel, aResultLength, aResult); } }; let loader = Components.classes["@mozilla.org/network/stream-loader;1"] .createInstance(Components.interfaces.nsIStreamLoader); loader.init(listener); httpChannel.asyncOpen(loader, httpChannel); } }, _handleHTTPResponse: function(aChannel, aResultLength, aResult) { let status; try { status = aChannel.responseStatus; if (status == 0) { status = 499; } } catch(e) { dump("sogoWebDAV: trapped exception: " + e + "\n"); setTimeout("throw new Error('sogoWebDAV could not download calendar. Try disabling proxy server.')",0); status = 499; } dump("GOT STATUS: " + status + "\n"); try { let headers = {}; let response = null; if (status == 499) { dump("xmlRequest: received status 499 for url: " + this.url + "\n"); } else { let visitor = {}; visitor.visitHeader = function(aHeader, aValue) { let key = aHeader.toLowerCase(); let array = headers[key]; if (!array) { array = []; headers[key] = array; } array.push(aValue.replace(/(^[ ]+|[ ]+$)/, "", "g")); }; aChannel.visitResponseHeaders(visitor); if (aResultLength > 0) { let responseText; if (typeof(aResult) == "string") { responseText = aResult; } else { let resultConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); resultConverter.charset ="UTF-8"; responseText = resultConverter.convertFromByteArray(aResult, aResultLength); } if (this.requestJSONResponse || this.requestXMLResponse) { let flatCType = (headers["content-type"] ? headers["content-type"][0] : ""); if ((flatCType.indexOf("text/xml") == 0 || flatCType.indexOf("application/xml") == 0) && aResultLength > 0) { let xmlParser = Components.classes["@mozilla.org/xmlextras/domparser;1"] .createInstance(Components.interfaces.nsIDOMParser); let responseXML = xmlParser.parseFromString(responseText, "text/xml"); if (this.requestJSONResponse) { let parser = new XMLToJSONParser(responseXML); response = parser; } else { response = responseXML; } } } else { response = responseText; } } } if (this.target && this.target.onDAVQueryComplete) { this.target.onDAVQueryComplete(status, response, headers, this.cbData); } } catch(e) { dump("sogoWebDAV.js: an exception occured\n" + e + "\n" + e.fileName + ":" + e.lineNumber + "\n"); let uri = aChannel.URI; if (uri) { dump("url: " + uri.spec + "\n"); } } }, load: function(operation, parameters) { if (operation == "GET") { var headers = {}; if (parameters.accept !== null) { headers.accept = parameters.accept; } this._sendHTTPRequest(operation, null, headers); } else if (operation == "PUT" || operation == "POST") { if(parameters.contentType.indexOf("text/vcard") == 0) { if (this.cbData.data.getProperty("groupDavKey", "") == "") { dump("NOTICE: uploading new vcard with empty key\n"); this._sendHTTPRequest(operation, parameters.data, { "content-type": parameters.contentType, "If-None-Match": "*" }); } else { let oldDavVersion = this.cbData.data.getProperty("groupDavVersionPrev", "-1"); dump("NOTICE: uploading modified vcard with etag: " + oldDavVersion + "\n"); if (oldDavVersion != "-1") { this._sendHTTPRequest(operation, parameters.data, { "content-type": parameters.contentType, "If-Match": oldDavVersion }); } else { dump("NOTICE: uploading modified vcard without etag\n"); this._sendHTTPRequest(operation, parameters.data, { "content-type": parameters.contentType, "If-None-Match": "*" }); } } } else { this._sendHTTPRequest(operation, parameters.data, { "content-type": parameters.contentType }); } } else if (operation == "PROPFIND") { let headers = { "depth": (parameters.deep ? "1": "0"), "content-type": "application/xml; charset=utf8" }; let query = this._propfindQuery(parameters.props); this._sendHTTPRequest(operation, query, headers); } else if (operation == "REPORT") { let headers = { "depth": (parameters.deep ? "1": "0"), "Connection": "TE", "TE": "trailers", "content-type": "application/xml; charset=utf8" }; this._sendHTTPRequest(operation, parameters.query, headers); } else if (operation == "MKCOL") { this._sendHTTPRequest(operation, parameters); } else if (operation == "DELETE") { this._sendHTTPRequest(operation, parameters); } else if (operation == "PROPPATCH") { let headers = { "content-type": "application/xml; charset=utf8" }; this._sendHTTPRequest(operation, parameters, headers); } else if (operation == "OPTIONS") { this._sendHTTPRequest(operation, parameters); } else throw ("operation '" + operation + "' is not currently supported"); }, get: function(accept) { this.load("GET", {accept: accept}); }, put: function(data, contentType) { this.load("PUT", {data: data, contentType: contentType}); }, post: function(data, contentType) { if (typeof(contentType) == "undefined") { contentType = "application/xml; charset=utf8"; } this.load("POST", {data: data, contentType: contentType}); }, _propfindQuery: function(props) { let nsDict = { "DAV:": "D" }; let propPart = ""; let nsCount = 0; for each (let prop in props) { let propParts = prop.split(" "); let ns = propParts[0]; let nsS = nsDict[ns]; if (!nsS) { nsS = "x" + nsCount; nsDict[ns] = nsS; nsCount++; } propPart += "<" + nsS + ":" + propParts[1] + "/>"; } let query = ("\n" + "" + propPart + ""); return query; }, options: function() { this.load("OPTIONS"); }, propfind: function(props, deep) { if (typeof deep == "undefined") deep = true; this.load("PROPFIND", {props: props, deep: deep}); }, mkcol: function() { this.load("MKCOL"); }, delete: function() { this.load("DELETE"); }, report: function(query, deep) { if (typeof deep == "undefined") deep = true; this.load("REPORT", {query: query, deep: deep}); }, proppatch: function(query) { this.load("PROPPATCH", query); } };