summaryrefslogtreecommitdiff
path: root/calendar/components/caldav/calDavCalendar.js
diff options
context:
space:
mode:
Diffstat (limited to 'calendar/components/caldav/calDavCalendar.js')
-rw-r--r--calendar/components/caldav/calDavCalendar.js2991
1 files changed, 2991 insertions, 0 deletions
diff --git a/calendar/components/caldav/calDavCalendar.js b/calendar/components/caldav/calDavCalendar.js
new file mode 100644
index 000000000..44320e339
--- /dev/null
+++ b/calendar/components/caldav/calDavCalendar.js
@@ -0,0 +1,2991 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Timer.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+Components.utils.import("resource:///modules/OAuth2.jsm");
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calXMLUtils.jsm");
+Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
+Components.utils.import("resource://calendar/modules/calProviderUtils.jsm");
+Components.utils.import("resource://calendar/modules/calAuthUtils.jsm");
+Components.utils.import("resource://calendar/modules/calAsyncUtils.jsm");
+
+//
+// calDavCalendar.js
+//
+
+var xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>\n';
+
+var davNS = "DAV:";
+var caldavNS = "urn:ietf:params:xml:ns:caldav";
+var calservNS = "http://calendarserver.org/ns/";
+var MIME_TEXT_CALENDAR = "text/calendar; charset=utf-8";
+var MIME_TEXT_XML = "text/xml; charset=utf-8";
+
+var cIOL = Components.interfaces.calIOperationListener;
+
+function caldavNSResolver(prefix) {
+ /* eslint-disable id-length */
+ const namespaces = {
+ D: davNS,
+ C: caldavNS,
+ CS: calservNS
+ };
+ /* eslint-enable id-length */
+
+ return namespaces[prefix] || null;
+}
+
+function caldavXPath(aNode, aExpr, aType) {
+ return cal.xml.evalXPath(aNode, aExpr, caldavNSResolver, aType);
+}
+function caldavXPathFirst(aNode, aExpr, aType) {
+ return cal.xml.evalXPathFirst(aNode, aExpr, caldavNSResolver, aType);
+}
+
+function calDavCalendar() {
+ this.initProviderBase();
+ this.unmappedProperties = [];
+ this.mUriParams = null;
+ this.mItemInfoCache = {};
+ this.mDisabled = false;
+ this.mCalHomeSet = null;
+ this.mInboxUrl = null;
+ this.mOutboxUrl = null;
+ this.mCalendarUserAddress = null;
+ this.mPrincipalUrl = null;
+ this.mSenderAddress = null;
+ this.mHrefIndex = {};
+ this.mAuthScheme = null;
+ this.mAuthRealm = null;
+ this.mObserver = null;
+ this.mFirstRefreshDone = false;
+ this.mOfflineStorage = null;
+ this.mQueuedQueries = [];
+ this.mCtag = null;
+ this.mProposedCtag = null;
+
+ // By default, support both events and todos.
+ this.mGenerallySupportedItemTypes = ["VEVENT", "VTODO"];
+ this.mSupportedItemTypes = this.mGenerallySupportedItemTypes.slice(0);
+ this.mACLProperties = {};
+}
+
+// some shorthand
+var calICalendar = Components.interfaces.calICalendar;
+var calIErrors = Components.interfaces.calIErrors;
+var calIFreeBusyInterval = Components.interfaces.calIFreeBusyInterval;
+var calICalDavCalendar = Components.interfaces.calICalDavCalendar;
+
+// used in checking calendar URI for (Cal)DAV-ness
+var kDavResourceTypeNone = 0;
+var kDavResourceTypeCollection = 1;
+var kDavResourceTypeCalendar = 2;
+
+// used for etag checking
+var CALDAV_MODIFY_ITEM = "modify";
+var CALDAV_DELETE_ITEM = "delete";
+
+var calDavCalendarClassID = Components.ID("{a35fc6ea-3d92-11d9-89f9-00045ace3b8d}");
+var calDavCalendarInterfaces = [
+ Components.interfaces.calICalendarProvider,
+ Components.interfaces.nsIInterfaceRequestor,
+ Components.interfaces.calIFreeBusyProvider,
+ Components.interfaces.nsIChannelEventSink,
+ Components.interfaces.calIItipTransport,
+ Components.interfaces.calISchedulingSupport,
+ Components.interfaces.calICalendar,
+ Components.interfaces.calIChangeLog,
+ calICalDavCalendar,
+];
+calDavCalendar.prototype = {
+ __proto__: cal.ProviderBase.prototype,
+ classID: calDavCalendarClassID,
+ QueryInterface: XPCOMUtils.generateQI(calDavCalendarInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: calDavCalendarClassID,
+ contractID: "@mozilla.org/calendar/calendar;1?type=caldav",
+ classDescription: "Calendar CalDAV back-end",
+ interfaces: calDavCalendarInterfaces,
+ }),
+
+ // An array of components that are supported by the server. The default is
+ // to support VEVENT and VTODO, if queries for these components return a 4xx
+ // error, then they will be removed from this array.
+ mGenerallySupportedItemTypes: null,
+ mSupportedItemTypes: null,
+ suportedItemTypes: null,
+ get supportedItemTypes() {
+ return this.mSupportedItemTypes;
+ },
+
+ get isCached() {
+ return (this != this.superCalendar);
+ },
+
+ mLastRedirectStatus: null,
+
+ ensureTargetCalendar: function() {
+ if (!this.isCached && !this.mOfflineStorage) {
+ // If this is a cached calendar, the actual cache is taken care of
+ // by the calCachedCalendar facade. In any other case, we use a
+ // memory calendar to cache things.
+ this.mOfflineStorage = Components
+ .classes["@mozilla.org/calendar/calendar;1?type=memory"]
+ .createInstance(Components.interfaces.calISyncWriteCalendar);
+
+ this.mOfflineStorage.superCalendar = this;
+ this.mObserver = new calDavObserver(this);
+ this.mOfflineStorage.addObserver(this.mObserver);
+ this.mOfflineStorage.setProperty("relaxedMode", true);
+ }
+ },
+
+ //
+ // calICalendarProvider interface
+ //
+ get prefChromeOverlay() {
+ return null;
+ },
+
+ get displayName() {
+ return calGetString("calendar", "caldavName");
+ },
+
+ createCalendar: function() {
+ throw NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ deleteCalendar: function(cal, listener) {
+ throw NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ // calIChangeLog interface
+ get offlineStorage() {
+ return this.mOfflineStorage;
+ },
+
+ set offlineStorage(storage) {
+ this.mOfflineStorage = storage;
+ this.fetchCachedMetaData();
+ },
+
+ resetLog: function() {
+ if (this.isCached && this.mOfflineStorage) {
+ this.mOfflineStorage.startBatch();
+ try {
+ for (let itemId in this.mItemInfoCache) {
+ this.mOfflineStorage.deleteMetaData(itemId);
+ delete this.mItemInfoCache[itemId];
+ }
+ } finally {
+ this.mOfflineStorage.endBatch();
+ }
+ }
+ },
+
+ get offlineCachedProperties() {
+ return ["mAuthScheme", "mAuthRealm", "mHasWebdavSyncSupport",
+ "mCtag", "mWebdavSyncToken", "mSupportedItemTypes",
+ "mPrincipalUrl", "mCalHomeSet",
+ "mShouldPollInbox", "hasAutoScheduling", "mHaveScheduling",
+ "mCalendarUserAddress", "mOutboxUrl", "hasFreeBusy"];
+ },
+
+ get checkedServerInfo() {
+ if (Services.io.offline) {
+ return true;
+ } else {
+ return this.mCheckedServerInfo;
+ }
+ },
+
+ set checkedServerInfo(val) {
+ return (this.mCheckedServerInfo = val);
+ },
+
+ saveCalendarProperties: function() {
+ let properties = {};
+ for (let property of this.offlineCachedProperties) {
+ if (this[property] !== undefined) {
+ properties[property] = this[property];
+ }
+ }
+ this.mOfflineStorage.setMetaData("calendar-properties", JSON.stringify(properties));
+ },
+ restoreCalendarProperties: function(data) {
+ let properties = JSON.parse(data);
+ for (let property of this.offlineCachedProperties) {
+ if (properties[property] !== undefined) {
+ this[property] = properties[property];
+ }
+ }
+ },
+
+ // in calIGenericOperationListener aListener
+ replayChangesOn: function(aChangeLogListener) {
+ if (this.checkedServerInfo) {
+ this.safeRefresh(aChangeLogListener);
+ } else {
+ // If we haven't refreshed yet, then we should check the resource
+ // type first. This will call refresh() again afterwards.
+ this.setupAuthentication(aChangeLogListener);
+ }
+ },
+ setMetaData: function(id, path, etag, isInboxItem) {
+ if (this.mOfflineStorage.setMetaData) {
+ if (id) {
+ let dataString = [etag, path, isInboxItem ? "true" : "false"].join("\u001A");
+ this.mOfflineStorage.setMetaData(id, dataString);
+ } else {
+ cal.LOG("CalDAV: cannot store meta data without an id");
+ }
+ } else {
+ cal.ERROR("CalDAV: calendar storage does not support meta data");
+ }
+ },
+
+ /**
+ * Ensure that cached items have associated meta data, otherwise server side
+ * changes may not be reflected
+ */
+ ensureMetaData: function() {
+ let self = this;
+ let refreshNeeded = false;
+ let getMetaListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ for (let item of aItems) {
+ if (!(item.id in self.mItemInfoCache)) {
+ let path = self.getItemLocationPath(item);
+ cal.LOG("Adding meta-data for cached item " + item.id);
+ self.mItemInfoCache[item.id] = {
+ etag: null,
+ isNew: false,
+ locationPath: path,
+ isInboxItem: false
+ };
+ self.mHrefIndex[self.mLocationPath + path] = item.id;
+ refreshNeeded = true;
+ }
+ }
+ },
+ onOperationComplete: function(aCalendar, aStatus, aOpType, aId, aDetail) {
+ if (refreshNeeded) {
+ // reseting the cached ctag forces an item refresh when
+ // safeRefresh is called later
+ self.mCtag = null;
+ self.mProposedCtag = null;
+ }
+ }
+ };
+ this.mOfflineStorage.getItems(calICalendar.ITEM_FILTER_ALL_ITEMS,
+ 0, null, null, getMetaListener);
+ },
+
+ fetchCachedMetaData: function() {
+ cal.LOG("CalDAV: Retrieving server info from cache for " + this.name);
+ let cacheIds = {};
+ let cacheValues = {};
+ this.mOfflineStorage.getAllMetaData({}, cacheIds, cacheValues);
+ cacheIds = cacheIds.value;
+ cacheValues = cacheValues.value;
+
+ for (let count = 0; count < cacheIds.length; count++) {
+ let itemId = cacheIds[count];
+ let itemData = cacheValues[count];
+ if (itemId == "ctag") {
+ this.mCtag = itemData;
+ this.mProposedCtag = null;
+ this.mOfflineStorage.deleteMetaData("ctag");
+ } else if (itemId == "webdav-sync-token") {
+ this.mWebdavSyncToken = itemData;
+ this.mOfflineStorage.deleteMetaData("sync-token");
+ } else if (itemId == "calendar-properties") {
+ this.restoreCalendarProperties(itemData);
+ this.setProperty("currentStatus", Components.results.NS_OK);
+ if (this.mHaveScheduling || this.hasAutoScheduling || this.hasFreeBusy) {
+ cal.getFreeBusyService().addProvider(this);
+ }
+ } else {
+ let itemDataArray = itemData.split("\u001A");
+ let etag = itemDataArray[0];
+ let resourcePath = itemDataArray[1];
+ let isInboxItem = itemDataArray[2];
+ if (itemDataArray.length == 3) {
+ this.mHrefIndex[resourcePath] = itemId;
+ let locationPath = resourcePath
+ .substr(this.mLocationPath.length);
+ let item = {
+ etag: etag,
+ isNew: false,
+ locationPath: locationPath,
+ isInboxItem: isInboxItem == "true"
+ };
+ this.mItemInfoCache[itemId] = item;
+ }
+ }
+ }
+
+ this.ensureMetaData();
+ },
+
+ sendHttpRequest: function(aUri, aUploadData, aContentType, aExisting, aSetupChannelFunc, aFailureFunc, aUseStreamLoader=true) {
+ function oauthCheck(nextMethod, loaderOrRequest /* either the nsIStreamLoader or nsIRequestObserver parameters */) {
+ let request = (loaderOrRequest.request || loaderOrRequest).QueryInterface(Components.interfaces.nsIHttpChannel);
+ let error = false;
+ try {
+ let wwwauth = request.getResponseHeader("WWW-Authenticate");
+ if (wwwauth.startsWith("Bearer") && wwwauth.includes("error=")) {
+ // An OAuth error occurred, we need to reauthenticate.
+ error = true;
+ }
+ } catch (e) {
+ // This happens in case the response header is missing, thats fine.
+ }
+
+ if (self.oauth && error) {
+ self.oauth.accessToken = null;
+ self.sendHttpRequest(...origArgs);
+ } else {
+ let nextArguments = Array.slice(arguments, 1);
+ nextMethod(...nextArguments);
+ }
+ }
+
+ function authSuccess() {
+ let channel = cal.prepHttpChannel(aUri, aUploadData, aContentType, self, aExisting);
+ if (usesGoogleOAuth) {
+ let hdr = "Bearer " + self.oauth.accessToken;
+ channel.setRequestHeader("Authorization", hdr, false);
+ }
+ let listener = aSetupChannelFunc(channel);
+ if (aUseStreamLoader) {
+ let loader = cal.createStreamLoader();
+ listener.onStreamComplete = oauthCheck.bind(null, listener.onStreamComplete.bind(listener));
+ loader.init(listener);
+ listener = loader;
+ } else {
+ listener.onStartRequest = oauthCheck.bind(null, listener.onStartRequest.bind(listener));
+ }
+
+ self.mLastRedirectStatus = null;
+ channel.asyncOpen(listener, channel);
+ }
+
+ const OAUTH_GRACE_TIME = 30 * 1000;
+
+ let usesGoogleOAuth = aUri && aUri.host == "apidata.googleusercontent.com" && this.oauth;
+ let origArgs = arguments;
+ let self = this;
+
+ if (usesGoogleOAuth && (
+ !this.oauth.accessToken ||
+ this.oauth.tokenExpires - OAUTH_GRACE_TIME < (new Date()).getTime())) {
+ // The token has expired, we need to reauthenticate first
+ cal.LOG("CalDAV: OAuth token expired or empty, refreshing");
+ this.oauth.connect(authSuccess, aFailureFunc, true, true);
+ } else {
+ // Either not Google OAuth, or the token is still valid.
+ authSuccess();
+ }
+ },
+
+ //
+ // calICalendar interface
+ //
+
+ // readonly attribute AUTF8String type;
+ get type() { return "caldav"; },
+
+ mDisabled: true,
+
+ mCalendarUserAddress: null,
+ get calendarUserAddress() {
+ return this.mCalendarUserAddress;
+ },
+
+ mPrincipalUrl: null,
+ get principalUrl() {
+ return this.mPrincipalUrl;
+ },
+
+ get canRefresh() {
+ // A cached calendar doesn't need to be refreshed.
+ return !this.isCached;
+ },
+
+ // mUriParams stores trailing ?parameters from the
+ // supplied calendar URI. Needed for (at least) Cosmo
+ // tickets
+ mUriParams: null,
+
+ get uri() { return this.mUri; },
+
+ set uri(aUri) {
+ this.mUri = aUri;
+
+ return aUri;
+ },
+
+ get calendarUri() {
+ let calUri = this.mUri.clone();
+ let parts = calUri.spec.split("?");
+ if (parts.length > 1) {
+ calUri.spec = parts.shift();
+ this.mUriParams = "?" + parts.join("?");
+ }
+ if (calUri.spec.charAt(calUri.spec.length - 1) != "/") {
+ calUri.spec += "/";
+ }
+ return calUri;
+ },
+
+ setCalHomeSet: function(removeLastPathSegment) {
+ if (removeLastPathSegment) {
+ let calUri = this.mUri.clone();
+ let split1 = calUri.spec.split("?");
+ let baseUrl = split1[0];
+ if (baseUrl.charAt(baseUrl.length - 1) == "/") {
+ baseUrl = baseUrl.substring(0, baseUrl.length - 2);
+ }
+ let split2 = baseUrl.split("/");
+ split2.pop();
+ calUri.spec = split2.join("/") + "/";
+ this.mCalHomeSet = calUri;
+ } else {
+ this.mCalHomeSet = this.calendarUri;
+ }
+ },
+
+ mOutboxUrl: null,
+ get outboxUrl() {
+ return this.mOutboxUrl;
+ },
+
+ mInboxUrl: null,
+ get inboxUrl() {
+ return this.mInboxUrl;
+ },
+
+ mHaveScheduling: false,
+ mShouldPollInbox: true,
+ get hasScheduling() { // Whether to use inbox/outbox scheduling
+ return this.mHaveScheduling;
+ },
+ set hasScheduling(value) {
+ return (this.mHaveScheduling = (Preferences.get("calendar.caldav.sched.enabled", false) && value));
+ },
+ hasAutoScheduling: false, // Whether server automatically takes care of scheduling
+ hasFreebusy: false,
+
+ mAuthScheme: null,
+
+ mAuthRealm: null,
+
+ mFirstRefreshDone: false,
+
+ mQueuedQueries: null,
+
+ mCtag: null,
+ mProposedCtag: null,
+
+ mOfflineStorage: null,
+ // Contains the last valid synctoken returned
+ // from the server with Webdav Sync enabled servers
+ mWebdavSyncToken: null,
+ // Indicates that the server supports Webdav Sync
+ // see: http://tools.ietf.org/html/draft-daboo-webdav-sync
+ mHasWebdavSyncSupport: false,
+
+ get authRealm() {
+ return this.mAuthRealm;
+ },
+
+ /**
+ * Builds a correctly encoded nsIURI based on the baseUri and the insert
+ * string. The returned uri is basically the baseURI + aInsertString
+ *
+ * @param aInsertString String to append to the base uri, for example,
+ * when creating an event this would be the
+ * event file name (event.ics), if null, an empty
+ * string is used.
+ * @param aBaseUri base uri (nsIURI object), if null, this.calendarUri
+ * will be used.
+ */
+ makeUri: function(aInsertString, aBaseUri) {
+ let baseUri = aBaseUri || this.calendarUri;
+ // Build a string containing the full path, decoded, so it looks like
+ // this:
+ // /some path/insert string.ics
+ let decodedPath = this.ensureDecodedPath(baseUri.path) + (aInsertString || "");
+
+ // Build the nsIURI by specifying a string with a fully encoded path
+ // the end result will be something like this:
+ // http://caldav.example.com:8080/some%20path/insert%20string.ics
+ let url = cal.makeURL(baseUri.prePath + this.ensureEncodedPath(decodedPath) + (this.mUriParams || ""));
+ return url;
+ },
+
+ get mLocationPath() {
+ return this.ensureDecodedPath(this.calendarUri.path);
+ },
+
+ getItemLocationPath: function(aItem) {
+ if (aItem.id &&
+ aItem.id in this.mItemInfoCache &&
+ this.mItemInfoCache[aItem.id].locationPath) {
+ // modifying items use the cached location path
+ return this.mItemInfoCache[aItem.id].locationPath;
+ } else {
+ // New items just use id.ics
+ return aItem.id + ".ics";
+ }
+ },
+
+ getProperty: function(aName) {
+ if (aName in this.mACLProperties && this.mACLProperties[aName]) {
+ return this.mACLProperties[aName];
+ }
+
+ switch (aName) {
+ case "organizerId":
+ if (this.calendarUserAddress) {
+ return this.calendarUserAddress;
+ } // else use configured email identity
+ break;
+ case "organizerCN":
+ return null; // xxx todo
+ case "itip.transport":
+ if (this.hasAutoScheduling || this.hasScheduling) {
+ return this.QueryInterface(Components.interfaces.calIItipTransport);
+ } // else use outbound email-based iTIP (from cal.ProviderBase)
+ break;
+ case "capabilities.tasks.supported":
+ return this.supportedItemTypes.includes("VTODO");
+ case "capabilities.events.supported":
+ return this.supportedItemTypes.includes("VEVENT");
+ case "capabilities.autoschedule.supported":
+ return this.hasAutoScheduling;
+ }
+ return this.__proto__.__proto__.getProperty.apply(this, arguments);
+ },
+
+ promptOverwrite: function(aMethod, aItem, aListener, aOldItem) {
+ let overwrite = cal.promptOverwrite(aMethod, aItem, aListener, aOldItem);
+ if (overwrite) {
+ if (aMethod == CALDAV_MODIFY_ITEM) {
+ this.doModifyItem(aItem, aOldItem, aListener, true);
+ } else {
+ this.doDeleteItem(aItem, aListener, true, false, null);
+ }
+ } else {
+ this.getUpdatedItem(aItem, aListener);
+ }
+ },
+
+ mItemInfoCache: null,
+
+ mHrefIndex: null,
+
+ /**
+ * addItem()
+ * we actually use doAdoptItem()
+ *
+ * @param aItem item to add
+ * @param aListener listener for method completion
+ */
+ addItem: function(aItem, aListener) {
+ return this.doAdoptItem(aItem.clone(), aListener);
+ },
+
+ /**
+ * adoptItem()
+ * we actually use doAdoptItem()
+ *
+ * @param aItem item to check
+ * @param aListener listener for method completion
+ */
+ adoptItem: function(aItem, aListener) {
+ return this.doAdoptItem(aItem, aListener);
+ },
+
+ /**
+ * Performs the actual addition of the item to CalDAV store
+ *
+ * @param aItem item to add
+ * @param aListener listener for method completion
+ * @param aIgnoreEtag flag to indicate ignoring of Etag
+ */
+ doAdoptItem: function(aItem, aListener, aIgnoreEtag) {
+ let notifyListener = (status, detail, pure=false) => {
+ let method = pure ? "notifyPureOperationComplete" : "notifyOperationComplete";
+ this[method](aListener, status, cIOL.ADD, aItem.id, detail);
+ };
+ if (aItem.id == null && aItem.isMutable) {
+ aItem.id = cal.getUUID();
+ }
+
+ if (aItem.id == null) {
+ notifyListener(Components.results.NS_ERROR_FAILURE,
+ "Can't set ID on non-mutable item to addItem");
+ return;
+ }
+
+ if (!isItemSupported(aItem, this)) {
+ notifyListener(Components.results.NS_ERROR_FAILURE,
+ "Server does not support item type");
+ return;
+ }
+
+ let parentItem = aItem.parentItem;
+ parentItem.calendar = this.superCalendar;
+
+ let locationPath = this.getItemLocationPath(parentItem);
+ let itemUri = this.makeUri(locationPath);
+ cal.LOG("CalDAV: itemUri.spec = " + itemUri.spec);
+
+ let self = this;
+ let serializedItem = this.getSerializedItem(aItem);
+ let addListener = {
+ onStreamComplete: function(aLoader, aContext, aStatus, aResultLength, aResult) {
+ let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ let listenerStatus = Components.results.NS_OK;
+ let listenerDetail = parentItem;
+ let responseStatus;
+ try {
+ responseStatus = request.responseStatus;
+
+ if (self.verboseLogging()) {
+ let str = cal.convertByteArray(aResult, aResultLength);
+ cal.LOG("CalDAV: recv: " + (str || ""));
+ }
+ } catch (ex) {
+ listenerStatus = ex.result;
+ listenerDetail = "Request Failed: " + ex.message;
+ cal.LOG("CalDAV: Request error during add: " + ex);
+ }
+
+
+ // Translate the HTTP status code to a status and message for the listener
+ if (responseStatus == 201 || responseStatus == 204) {
+ // 201 = HTTP "Created"
+ // 204 = HTTP "No Content"
+ cal.LOG("CalDAV: Item added to " + self.name + " successfully");
+
+ let uriComponentParts = self.makeUri().path.replace(/\/{2,}/g, "/").split("/").length;
+ let targetParts = request.URI.path.split("/");
+ targetParts.splice(0, uriComponentParts - 1);
+
+ self.mItemInfoCache[parentItem.id] = { locationPath: targetParts.join("/") };
+ // TODO: onOpComplete adds the item to the cache, probably after getUpdatedItem!
+
+ // Some CalDAV servers will modify items on PUT (add X-props,
+ // for instance) so we'd best re-fetch in order to know
+ // the current state of the item
+ // Observers will be notified in getUpdatedItem()
+ self.getUpdatedItem(parentItem, aListener);
+ return;
+ } else if (responseStatus >= 500 && responseStatus <= 510) {
+ listenerStatus = Components.results.NS_ERROR_NOT_AVAILABLE;
+ listenerDetail = "Server Replied with " + responseStatus;
+ } else if (responseStatus) {
+ // There is a response status, but we haven't handled it yet. Any
+ // error occurring here should consider being handled!
+ cal.ERROR("CalDAV: Unexpected status adding item to " +
+ self.name + ": " + responseStatus + "\n" +
+ serializedItem);
+
+ listenerStatus = Components.results.NS_ERROR_FAILURE;
+ listenerDetail = "Server Replied with " + responseStatus;
+ }
+
+ // Still need to visually notify for uncached calendars.
+ if (!self.isCached && !Components.isSuccessCode(listenerStatus)) {
+ self.reportDavError(calIErrors.DAV_PUT_ERROR, listenerStatus, listenerDetail);
+ }
+
+ // Finally, notify listener.
+ notifyListener(listenerStatus, listenerDetail, true);
+ }
+ };
+
+ this.sendHttpRequest(itemUri, serializedItem, MIME_TEXT_CALENDAR, null, (channel) => {
+ if (!aIgnoreEtag) {
+ channel.setRequestHeader("If-None-Match", "*", false);
+ }
+ return addListener;
+ }, () => {
+ notifyListener(Components.results.NS_ERROR_NOT_AVAILABLE,
+ "Error preparing http channel");
+ });
+ },
+
+ /**
+ * modifyItem(); required by calICalendar.idl
+ * we actually use doModifyItem()
+ *
+ * @param aItem item to check
+ * @param aListener listener for method completion
+ */
+ modifyItem: function(aNewItem, aOldItem, aListener) {
+ return this.doModifyItem(aNewItem, aOldItem, aListener, false);
+ },
+
+ /**
+ * Modifies existing item in CalDAV store.
+ *
+ * @param aItem item to check
+ * @param aOldItem previous version of item to be modified
+ * @param aListener listener from original request
+ * @param aIgnoreEtag ignore item etag
+ */
+ doModifyItem: function(aNewItem, aOldItem, aListener, aIgnoreEtag) {
+ let notifyListener = (status, detail, pure=false) => {
+ let method = pure ? "notifyPureOperationComplete" : "notifyOperationComplete";
+ this[method](aListener, status, cIOL.MODIFY, aNewItem.id, detail);
+ };
+ if (aNewItem.id == null) {
+ notifyListener(Components.results.NS_ERROR_FAILURE,
+ "ID for modifyItem doesn't exist or is null");
+ return;
+ }
+
+ let wasInboxItem = this.mItemInfoCache[aNewItem.id].isInboxItem;
+
+ let newItem_ = aNewItem;
+ aNewItem = aNewItem.parentItem.clone();
+ if (newItem_.parentItem != newItem_) {
+ aNewItem.recurrenceInfo.modifyException(newItem_, false);
+ }
+ aNewItem.generation += 1;
+
+ let eventUri = this.makeUri(this.mItemInfoCache[aNewItem.id].locationPath);
+
+ let self = this;
+
+ let modifiedItemICS = this.getSerializedItem(aNewItem);
+
+ let modListener = {
+ onStreamComplete: function(aLoader, aContext, aStatus, aResultLength, aResult) {
+ let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ let listenerStatus = Components.results.NS_OK;
+ let listenerDetail = aNewItem;
+ let responseStatus;
+ try {
+ responseStatus = request.responseStatus;
+
+ if (self.verboseLogging()) {
+ let str = cal.convertByteArray(aResult, aResultLength);
+ cal.LOG("CalDAV: recv: " + (str || ""));
+ }
+ } catch (ex) {
+ listenerStatus = ex.result;
+ listenerDetail = "Request Failed: " + ex.message;
+ cal.LOG("CalDAV: Request error during add: " + ex);
+ }
+
+ if (responseStatus == 204 || responseStatus == 201 || responseStatus == 200) {
+ // We should not accept a 201 status here indefinitely: it indicates a server error
+ // of some kind that we want to know about. It's convenient to accept it for now
+ // since a number of server impls don't get this right yet.
+ cal.LOG("CalDAV: Item modified successfully on " + self.name);
+
+ // Some CalDAV servers will modify items on PUT (add X-props,
+ // for instance) so we'd best re-fetch in order to know
+ // the current state of the item
+ // Observers will be notified in getUpdatedItem()
+ self.getUpdatedItem(aNewItem, aListener);
+
+ // SOGo has calendarUri == inboxUri so we need to be careful
+ // about deletions
+ if (wasInboxItem && self.mShouldPollInbox) {
+ self.doDeleteItem(aNewItem, null, true, true, null);
+ }
+ return;
+ } else if (responseStatus == 412 || responseStatus == 409) {
+ // promptOverwrite will ask the user and then re-request
+ self.promptOverwrite(CALDAV_MODIFY_ITEM, aNewItem,
+ aListener, aOldItem);
+ return;
+ } else if (responseStatus >= 500 && responseStatus <= 510) {
+ listenerStatus = Components.results.NS_ERROR_NOT_AVAILABLE;
+ listenerDetail = "Server Replied with " + responseStatus;
+ } else if (responseStatus) {
+ // There is a response status, but we haven't handled it yet. Any
+ // error occurring here should consider being handled!
+ cal.ERROR("CalDAV: Unexpected status modifying item to " +
+ self.name + ": " + responseStatus + "\n" +
+ modifiedItemICS);
+
+ listenerStatus = Components.results.NS_ERROR_FAILURE;
+ listenerDetail = "Server Replied with " + responseStatus;
+ }
+
+ // Still need to visually notify for uncached calendars.
+ if (!self.isCached && !Components.isSuccessCode(listenerStatus)) {
+ self.reportDavError(calIErrors.DAV_PUT_ERROR, listenerStatus, listenerDetail);
+ }
+
+ notifyListener(listenerStatus, listenerDetail, true);
+ }
+ };
+
+ this.sendHttpRequest(eventUri, modifiedItemICS, MIME_TEXT_CALENDAR, null, (channel) => {
+ if (!aIgnoreEtag) {
+ channel.setRequestHeader("If-Match",
+ this.mItemInfoCache[aNewItem.id].etag,
+ false);
+ }
+ return modListener;
+ }, () => {
+ notifyListener(Components.results.NS_ERROR_NOT_AVAILABLE,
+ "Error preparing http channel");
+ });
+ },
+
+ /**
+ * deleteItem(); required by calICalendar.idl
+ * the actual deletion is done in doDeleteItem()
+ *
+ * @param aItem item to delete
+ * @param aListener listener for method completion
+ */
+ deleteItem: function(aItem, aListener) {
+ return this.doDeleteItem(aItem, aListener, false, null, null);
+ },
+
+ /**
+ * Deletes item from CalDAV store.
+ *
+ * @param aItem item to delete
+ * @param aListener listener for method completion
+ * @param aIgnoreEtag ignore item etag
+ * @param aFromInbox delete from inbox rather than calendar
+ * @param aUri uri of item to delete
+ * */
+ doDeleteItem: function(aItem, aListener, aIgnoreEtag, aFromInbox, aUri) {
+ let notifyListener = (status, detail, pure=false) => {
+ let method = pure ? "notifyPureOperationComplete" : "notifyOperationComplete";
+ this[method](aListener, status, cIOL.DELETE, aItem.id, detail);
+ };
+
+ if (aItem.id == null) {
+ notifyListener(Components.results.NS_ERROR_FAILURE,
+ "ID doesn't exist for deleteItem");
+ return;
+ }
+
+ let eventUri;
+ if (aUri) {
+ eventUri = aUri;
+ } else if (aFromInbox || this.mItemInfoCache[aItem.id].isInboxItem) {
+ eventUri = this.makeUri(this.mItemInfoCache[aItem.id].locationPath, this.mInboxUrl);
+ } else {
+ eventUri = this.makeUri(this.mItemInfoCache[aItem.id].locationPath);
+ }
+
+ if (eventUri.path == this.calendarUri.path) {
+ notifyListener(Components.results.NS_ERROR_FAILURE,
+ "eventUri and calendarUri paths are the same, " +
+ "will not go on to delete entire calendar");
+ return;
+ }
+
+ let self = this;
+
+ let delListener = {
+ onStreamComplete: function(aLoader, aContext, aStatus, aResultLength, aResult) {
+ let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ let listenerStatus = Components.results.NS_OK;
+ let listenerDetail = aItem;
+ let responseStatus;
+ try {
+ responseStatus = request.responseStatus;
+
+ if (self.verboseLogging()) {
+ let str = cal.convertByteArray(aResult, aResultLength);
+ cal.LOG("CalDAV: recv: " + (str || ""));
+ }
+ } catch (ex) {
+ listenerStatus = ex.result;
+ listenerDetail = "Request Failed: " + ex.message;
+ cal.LOG("CalDAV: Request error during delete: " + ex);
+ }
+
+ // 204 = HTTP "No content"
+ // 404 = Not Found - This is kind of a success, since the item is already deleted.
+ //
+ if (responseStatus == 204 || responseStatus == 200 || responseStatus == 404) {
+ if (!aFromInbox) {
+ let decodedPath = self.ensureDecodedPath(eventUri.path);
+ delete self.mHrefIndex[decodedPath];
+ delete self.mItemInfoCache[aItem.id];
+ cal.LOG("CalDAV: Item deleted successfully from calendar " + self.name);
+
+ if (!self.isCached) {
+ // If the calendar is not cached, we need to remove
+ // the item from our memory calendar now. The
+ // listeners will be notified there.
+ self.mOfflineStorage.deleteItem(aItem, aListener);
+ return;
+ }
+ }
+ } else if (responseStatus == 412 || responseStatus == 409) {
+ // item has either been modified or deleted by someone else check to see which
+ cal.LOG("CalDAV: Item has been modified on server, checking if it has been deleted");
+ self.sendHttpRequest(eventUri, null, null, null, (channel) => {
+ channel.requestMethod = "HEAD";
+ return delListener2;
+ }, () => {
+ notifyListener(Components.results.NS_ERROR_NOT_AVAILABLE,
+ "Error preparing http channel");
+ });
+ return;
+ } else if (responseStatus >= 500 && responseStatus <= 510) {
+ listenerStatus = Components.results.NS_ERROR_NOT_AVAILABLE;
+ listenerDetail = "Server Replied with " + responseStatus;
+ } else if (responseStatus) {
+ cal.ERROR("CalDAV: Unexpected status deleting item from " +
+ self.name + ": " + responseStatus + "\n" +
+ "uri: " + eventUri.spec);
+
+ listenerStatus = Components.results.NS_ERROR_FAILURE;
+ listenerDetail = "Server Replied with " + responseStatus;
+ }
+
+ // Still need to visually notify for uncached calendars.
+ if (!self.isCached && !Components.isSuccessCode(listenerStatus)) {
+ self.reportDavError(calIErrors.DAV_REMOVE_ERROR, listenerStatus, listenerDetail);
+ }
+
+ // Finally, notify listener.
+ notifyListener(listenerStatus, listenerDetail);
+ }
+ };
+
+ let delListener2 = {
+ onStreamComplete: function(aLoader, aContext, aStatus, aResultLength, aResult) {
+ let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ let listenerStatus = Components.results.NS_OK;
+ let listenerDetail = aItem;
+ let responseStatus;
+ try {
+ responseStatus = request.responseStatus;
+
+ if (self.verboseLogging()) {
+ let str = cal.convertByteArray(aResult, aResultLength);
+ cal.LOG("CalDAV: recv: " + (str || ""));
+ }
+ } catch (ex) {
+ listenerStatus = ex.result;
+ listenerDetail = "Request Failed: " + ex.message;
+ cal.LOG("CalDAV: Request error during add: " + ex);
+ }
+
+ if (responseStatus == 404) {
+ // Nothing to do (except notify the listener below)
+ // Someone else has already deleted it
+ } else if (responseStatus >= 500 && responseStatus <= 510) {
+ listenerStatus = Components.results.NS_ERROR_NOT_AVAILABLE;
+ listenerDetail = "Server Replied with " + responseStatus;
+ } else if (responseStatus) {
+ // The item still exists. We need to ask the user if he
+ // really wants to delete the item. Remember, we only
+ // made this request since the actual delete gave 409/412
+ self.promptOverwrite(CALDAV_DELETE_ITEM, aItem, aListener, null);
+ return;
+ }
+
+ // Finally, notify listener.
+ notifyListener(listenerStatus, listenerDetail, true);
+ }
+ };
+
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: Deleting " + eventUri.spec);
+ }
+
+ this.sendHttpRequest(eventUri, null, null, null, (channel) => {
+ if (!aIgnoreEtag) {
+ let etag = this.mItemInfoCache[aItem.id].etag;
+ cal.LOG("CalDAV: Will only delete if matches etag " + etag);
+ channel.setRequestHeader("If-Match", etag, false);
+ }
+ channel.requestMethod = "DELETE";
+ return delListener;
+ }, () => {
+ notifyListener(Components.results.NS_ERROR_NOT_AVAILABLE,
+ "Error preparing http channel");
+ });
+ },
+
+ /**
+ * Add an item to the target calendar
+ *
+ * @param path Item path MUST NOT BE ENCODED
+ * @param calData iCalendar string representation of the item
+ * @param aUri Base URI of the request
+ * @param aListener Listener
+ */
+ addTargetCalendarItem: function(path, calData, aUri, etag, aListener) {
+ let parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
+ .createInstance(Components.interfaces.calIIcsParser);
+ // aUri.path may contain double slashes whereas path does not
+ // this confuses our counting, so remove multiple successive slashes
+ let strippedUriPath = aUri.path.replace(/\/{2,}/g, "/");
+ let uriPathComponentLength = strippedUriPath.split("/").length;
+ try {
+ parser.parseString(calData);
+ } catch (e) {
+ // Warn and continue.
+ // TODO As soon as we have activity manager integration,
+ // this should be replace with logic to notify that a
+ // certain event failed.
+ cal.WARN("Failed to parse item: " + calData + "\n\nException:" + e);
+ return;
+ }
+ // with CalDAV there really should only be one item here
+ let items = parser.getItems({});
+ let propertiesList = parser.getProperties({});
+ let method;
+ for (let prop of propertiesList) {
+ if (prop.propertyName == "METHOD") {
+ method = prop.value;
+ break;
+ }
+ }
+ let isReply = (method == "REPLY");
+ let item = items[0];
+ if (!item) {
+ cal.WARN("Failed to parse item: " + calData);
+ return;
+ }
+
+ item.calendar = this.superCalendar;
+ if (isReply && this.isInbox(aUri.spec)) {
+ if (this.hasScheduling) {
+ this.processItipReply(item, path);
+ }
+ cal.WARN("REPLY method but calendar does not support scheduling");
+ return;
+ }
+
+ // Strip of the same number of components as the request
+ // uri's path has. This way we make sure to handle servers
+ // that pass paths like /dav/user/Calendar while
+ // the request uri is like /dav/user@example.org/Calendar.
+ let resPathComponents = path.split("/");
+ resPathComponents.splice(0, uriPathComponentLength - 1);
+ let locationPath = resPathComponents.join("/");
+ let isInboxItem = this.isInbox(aUri.spec);
+ let self = this;
+
+ Task.spawn(function* () {
+ if (self.mHrefIndex[path] &&
+ !self.mItemInfoCache[item.id]) {
+ // If we get here it means a meeting has kept the same filename
+ // but changed its uid, which can happen server side.
+ // Delete the meeting before re-adding it
+ self.deleteTargetCalendarItem(path);
+ }
+
+ if (self.mItemInfoCache[item.id]) {
+ self.mItemInfoCache[item.id].isNew = false;
+ } else {
+ self.mItemInfoCache[item.id] = { isNew: true };
+ }
+ self.mItemInfoCache[item.id].locationPath = locationPath;
+ self.mItemInfoCache[item.id].isInboxItem = isInboxItem;
+
+ self.mHrefIndex[path] = item.id;
+ self.mItemInfoCache[item.id].etag = etag;
+
+ let needsAddModify = false;
+ if (self.isCached) {
+ self.setMetaData(item.id, path, etag, isInboxItem);
+
+ // If we have a listener, then the caller will take care of adding the item
+ // Otherwise, we have to do it ourself
+ // XXX This is quite fragile, but saves us a double modify/add
+
+ if (aListener) {
+ // In the cached case, notifying operation complete will add the item to the cache
+ if (self.mItemInfoCache[item.id].isNew) {
+ self.notifyOperationComplete(aListener,
+ Components.results.NS_OK,
+ cIOL.ADD,
+ item.id,
+ item);
+ } else {
+ self.notifyOperationComplete(aListener,
+ Components.results.NS_OK,
+ cIOL.MODIFY,
+ item.id,
+ item);
+ }
+ } else {
+ // No listener, we'll have to add it ourselves
+ needsAddModify = true;
+ }
+ } else {
+ // In the uncached case, we need to do so ourselves
+ needsAddModify = true;
+ }
+
+ // Now take care of the add/modify if needed.
+ if (needsAddModify) {
+ if (self.mItemInfoCache[item.id].isNew) {
+ self.mOfflineStorage.adoptItem(item, aListener);
+ } else {
+ self.mOfflineStorage.modifyItem(item, null, aListener);
+ }
+ }
+ });
+ },
+
+ /**
+ * Deletes an item from the target calendar
+ *
+ * @param path Path of the item to delete, must not be encoded
+ */
+ deleteTargetCalendarItem: Task.async(function* (path) {
+ let pcal = cal.async.promisifyCalendar(this.mOfflineStorage);
+
+ let foundItem = (yield pcal.getItem(this.mHrefIndex[path]))[0];
+ let wasInboxItem = this.mItemInfoCache[foundItem.id].isInboxItem;
+ if ((wasInboxItem && this.isInbox(path)) ||
+ (wasInboxItem === false && !this.isInbox(path))) {
+ cal.LOG("CalDAV: deleting item: " + path + ", uid: " + foundItem.id);
+ delete this.mHrefIndex[path];
+ delete this.mItemInfoCache[foundItem.id];
+ if (this.isCached) {
+ this.mOfflineStorage.deleteMetaData(foundItem.id);
+ }
+ yield pcal.deleteItem(foundItem);
+ }
+ }),
+
+ /**
+ * Perform tasks required after updating items in the calendar such as
+ * notifying the observers and listeners
+ *
+ * @param aChangeLogListener Change log listener
+ * @param calendarURI URI of the calendar whose items just got
+ * changed
+ */
+ finalizeUpdatedItems: function(aChangeLogListener, calendarURI) {
+ cal.LOG("aChangeLogListener=" + aChangeLogListener + "\n" +
+ "calendarURI=" + (calendarURI ? calendarURI.spec : "undefined") + " \n" +
+ "iscached=" + this.isCached + "\n" +
+ "this.mQueuedQueries.length=" + this.mQueuedQueries.length);
+ if (this.isCached) {
+ if (aChangeLogListener) {
+ aChangeLogListener.onResult({ status: Components.results.NS_OK },
+ Components.results.NS_OK);
+ }
+ } else {
+ this.mObservers.notify("onLoad", [this]);
+ }
+
+ if (this.mProposedCtag) {
+ this.mCtag = this.mProposedCtag;
+ this.mProposedCtag = null;
+ }
+
+ this.mFirstRefreshDone = true;
+ while (this.mQueuedQueries.length) {
+ let query = this.mQueuedQueries.pop();
+ this.mOfflineStorage.getItems(...query);
+ }
+ if (this.hasScheduling &&
+ !this.isInbox(calendarURI.spec)) {
+ this.pollInbox();
+ }
+ },
+
+ /**
+ * Notifies the caller that a get request has failed.
+ *
+ * @param errorMsg Error message
+ * @param aListener (optional) Listener of the request
+ * @param aChangeLogListener (optional)Listener for cached calendars
+ */
+ notifyGetFailed: function(errorMsg, aListener, aChangeLogListener) {
+ cal.WARN("CalDAV: Get failed: " + errorMsg);
+
+ // Notify changelog listener
+ if (this.isCached && aChangeLogListener) {
+ aChangeLogListener.onResult({ status: Components.results.NS_ERROR_FAILURE },
+ Components.results.NS_ERROR_FAILURE);
+ }
+
+ // Notify operation listener
+ this.notifyOperationComplete(aListener,
+ Components.results.NS_ERROR_FAILURE,
+ cIOL.GET,
+ null,
+ errorMsg);
+ // If an error occurrs here, we also need to unqueue the
+ // requests previously queued.
+ while (this.mQueuedQueries.length) {
+ let [, , , , listener] = this.mQueuedQueries.pop();
+ try {
+ listener.onOperationComplete(this.superCalendar,
+ Components.results.NS_ERROR_FAILURE,
+ cIOL.GET,
+ null,
+ errorMsg);
+ } catch (e) {
+ cal.ERROR(e);
+ }
+ }
+ },
+
+ /**
+ * Retrieves a specific item from the CalDAV store.
+ * Use when an outdated copy of the item is in hand.
+ *
+ * @param aItem item to fetch
+ * @param aListener listener for method completion
+ */
+ getUpdatedItem: function(aItem, aListener, aChangeLogListener) {
+ if (aItem == null) {
+ this.notifyOperationComplete(aListener,
+ Components.results.NS_ERROR_FAILURE,
+ cIOL.GET,
+ null,
+ "passed in null item");
+ return;
+ }
+
+ let locationPath = this.getItemLocationPath(aItem);
+ let itemUri = this.makeUri(locationPath);
+
+ let multiget = new multigetSyncHandler([this.ensureDecodedPath(itemUri.path)],
+ this,
+ this.makeUri(),
+ null,
+ false,
+ aListener,
+ aChangeLogListener);
+ multiget.doMultiGet();
+ },
+
+ // void getItem( in string id, in calIOperationListener aListener );
+ getItem: function(aId, aListener) {
+ this.mOfflineStorage.getItem(aId, aListener);
+ },
+
+ // void getItems( in unsigned long aItemFilter, in unsigned long aCount,
+ // in calIDateTime aRangeStart, in calIDateTime aRangeEnd,
+ // in calIOperationListener aListener );
+ getItems: function(aItemFilter, aCount, aRangeStart, aRangeEnd, aListener) {
+ if (this.isCached) {
+ if (this.mOfflineStorage) {
+ this.mOfflineStorage.getItems(...arguments);
+ } else {
+ this.notifyOperationComplete(aListener,
+ Components.results.NS_OK,
+ cIOL.GET,
+ null,
+ null);
+ }
+ } else if (this.checkedServerInfo) {
+ this.mOfflineStorage.getItems(...arguments);
+ } else {
+ this.mQueuedQueries.push(Array.from(arguments));
+ }
+ },
+
+ fillACLProperties: function() {
+ let orgId = this.calendarUserAddress;
+ if (orgId) {
+ this.mACLProperties.organizerId = orgId;
+ }
+
+ if (this.mACLEntry && this.mACLEntry.hasAccessControl) {
+ let ownerIdentities = this.mACLEntry.getOwnerIdentities({});
+ if (ownerIdentities.length > 0) {
+ let identity = ownerIdentities[0];
+ this.mACLProperties.organizerId = identity.email;
+ this.mACLProperties.organizerCN = identity.fullName;
+ this.mACLProperties["imip.identity"] = identity;
+ }
+ }
+ },
+
+ safeRefresh: function(aChangeLogListener) {
+ let notifyListener = (status) => {
+ if (this.isCached && aChangeLogListener) {
+ aChangeLogListener.onResult({ status: status }, status);
+ }
+ };
+
+ if (!this.mACLEntry) {
+ let self = this;
+ let opListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onGetResult: function(calendar, status, itemType, detail, count, items) {
+ ASSERT(false, "unexpected!");
+ },
+ onOperationComplete: function(opCalendar, opStatus, opType, opId, opDetail) {
+ self.mACLEntry = opDetail;
+ self.fillACLProperties();
+ self.safeRefresh(aChangeLogListener);
+ }
+ };
+
+ this.aclManager.getCalendarEntry(this, opListener);
+ return;
+ }
+
+ this.ensureTargetCalendar();
+
+ if (this.mAuthScheme == "Digest") {
+ // the auth could have timed out and be in need of renegotiation
+ // we can't risk several calendars doing this simultaneously so
+ // we'll force the renegotiation in a sync query, using OPTIONS to keep
+ // it quick
+ let headchannel = cal.prepHttpChannel(this.makeUri(), null, null, this);
+ headchannel.requestMethod = "OPTIONS";
+ headchannel.open();
+ headchannel.QueryInterface(Components.interfaces.nsIHttpChannel);
+ try {
+ if (headchannel.responseStatus != 200) {
+ throw "OPTIONS returned unexpected status code: " + headchannel.responseStatus;
+ }
+ } catch (e) {
+ cal.WARN("CalDAV: Exception: " + e);
+ notifyListener(Components.results.NS_ERROR_FAILURE);
+ }
+ }
+
+ // Call getUpdatedItems right away if its the first refresh
+ // *OR* if webdav Sync is enabled (It is redundant to send a request
+ // to get the collection tag (getctag) on a calendar if it supports
+ // webdav sync, the sync request will only return data if something
+ // changed).
+ if (!this.mCtag || !this.mFirstRefreshDone || this.mHasWebdavSyncSupport) {
+ this.getUpdatedItems(this.calendarUri, aChangeLogListener);
+ return;
+ }
+ let self = this;
+ let queryXml =
+ xmlHeader +
+ '<D:propfind xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">' +
+ "<D:prop>" +
+ "<CS:getctag/>" +
+ "</D:prop>" +
+ "</D:propfind>";
+
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: send(" + this.makeUri().spec + "): " + queryXml);
+ }
+
+ let streamListener = {};
+ streamListener.onStreamComplete = function(aLoader, aContext, aStatus, aResultLength, aResult) {
+ let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ try {
+ cal.LOG("CalDAV: Status " + request.responseStatus +
+ " checking ctag for calendar " + self.name);
+ } catch (ex) {
+ cal.LOG("CalDAV: Error without status on checking ctag for calendar " +
+ self.name);
+ notifyListener(Components.results.NS_OK);
+ return;
+ }
+
+ if (request.responseStatus == 404) {
+ cal.LOG("CalDAV: Disabling calendar " + self.name +
+ " due to 404");
+ notifyListener(Components.results.NS_ERROR_FAILURE);
+ return;
+ } else if (request.responseStatus == 207 && self.mDisabled) {
+ // Looks like the calendar is there again, check its resource
+ // type first.
+ self.setupAuthentication(aChangeLogListener);
+ return;
+ }
+
+ let str = cal.convertByteArray(aResult, aResultLength);
+ if (!str) {
+ cal.LOG("CalDAV: Failed to get ctag from server for calendar " +
+ self.name);
+ } else if (self.verboseLogging()) {
+ cal.LOG("CalDAV: recv: " + str);
+ }
+
+ let multistatus;
+ try {
+ multistatus = cal.xml.parseString(str);
+ } catch (ex) {
+ cal.LOG("CalDAV: Failed to get ctag from server for calendar " +
+ self.name);
+ notifyListener(Components.results.NS_OK);
+ return;
+ }
+
+ let ctag = caldavXPathFirst(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/CS:getctag/text()");
+ if (!ctag || ctag != self.mCtag) {
+ // ctag mismatch, need to fetch calendar-data
+ self.mProposedCtag = ctag;
+ self.getUpdatedItems(self.calendarUri, aChangeLogListener);
+ if (self.verboseLogging()) {
+ cal.LOG("CalDAV: ctag mismatch on refresh, fetching data for " +
+ "calendar " + self.name);
+ }
+ } else {
+ if (self.verboseLogging()) {
+ cal.LOG("CalDAV: ctag matches, no need to fetch data for " +
+ "calendar " + self.name);
+ }
+
+ // Notify the listener, but don't return just yet...
+ notifyListener(Components.results.NS_OK);
+
+ // ...we may still need to poll the inbox
+ if (self.firstInRealm()) {
+ self.pollInbox();
+ }
+ }
+ };
+
+ this.sendHttpRequest(this.makeUri(), queryXml, MIME_TEXT_XML, null, (channel) => {
+ channel.setRequestHeader("Depth", "0", false);
+ channel.requestMethod = "PROPFIND";
+ return streamListener;
+ }, () => {
+ notifyListener(Components.results.NS_ERROR_NOT_AVAILABLE);
+ });
+ },
+
+ refresh: function() {
+ this.replayChangesOn(null);
+ },
+
+ firstInRealm: function() {
+ let calendars = getCalendarManager().getCalendars({});
+ for (let i = 0; i < calendars.length; i++) {
+ if (calendars[i].type != "caldav" || calendars[i].getProperty("disabled")) {
+ continue;
+ }
+ // XXX We should probably expose the inner calendar via an
+ // interface, but for now use wrappedJSObject.
+ let calendar = calendars[i].wrappedJSObject;
+ if (calendar.mUncachedCalendar) {
+ calendar = calendar.mUncachedCalendar;
+ }
+ if (calendar.uri.prePath == this.uri.prePath &&
+ calendar.authRealm == this.mAuthRealm) {
+ if (calendar.id == this.id) {
+ return true;
+ }
+ break;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Get updated items
+ *
+ * @param aUri The uri to request the items from.
+ * NOTE: This must be the uri without any uri
+ * params. They will be appended in this
+ * function.
+ * @param aChangeLogListener (optional) The listener to notify for cached
+ * calendars.
+ */
+ getUpdatedItems: function(aUri, aChangeLogListener) {
+ if (this.mDisabled) {
+ // check if maybe our calendar has become available
+ this.setupAuthentication(aChangeLogListener);
+ return;
+ }
+
+ if (this.mHasWebdavSyncSupport) {
+ webDavSync = new webDavSyncHandler(this, aUri, aChangeLogListener);
+ webDavSync.doWebDAVSync();
+ return;
+ }
+
+ let queryXml =
+ xmlHeader +
+ '<D:propfind xmlns:D="DAV:">' +
+ "<D:prop>" +
+ "<D:getcontenttype/>" +
+ "<D:resourcetype/>" +
+ "<D:getetag/>" +
+ "</D:prop>" +
+ "</D:propfind>";
+
+ let requestUri = this.makeUri(null, aUri);
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryXml);
+ }
+
+ this.sendHttpRequest(requestUri, queryXml, MIME_TEXT_XML, null, (channel) => {
+ channel.requestMethod = "PROPFIND";
+ channel.setRequestHeader("Depth", "1", false);
+ return new etagsHandler(this, aUri, aChangeLogListener);
+ }, () => {
+ if (aChangeLogListener && this.isCached) {
+ aChangeLogListener.onResult({ status: Components.results.NS_ERROR_NOT_AVAILABLE },
+ Components.results.NS_ERROR_NOT_AVAILABLE);
+ }
+ }, false);
+ },
+
+ /**
+ * @see nsIInterfaceRequestor
+ * @see calProviderUtils.jsm
+ */
+ getInterface: cal.InterfaceRequestor_getInterface,
+
+ //
+ // Helper functions
+ //
+
+ /**
+ * Sets up any needed prerequisites regarding authentication. This is the
+ * beginning of a chain of asynchronous calls. This function will, when
+ * done, call the next function related to checking resource type, server
+ * capabilties, etc.
+ *
+ * setupAuthentication * You are here
+ * checkDavResourceType
+ * checkServerCaps
+ * findPrincipalNS
+ * checkPrincipalsNameSpace
+ * completeCheckServerInfo
+ */
+ setupAuthentication: function(aChangeLogListener) {
+ let self = this;
+ function authSuccess() {
+ self.checkDavResourceType(aChangeLogListener);
+ }
+ function authFailed() {
+ self.setProperty("disabled", "true");
+ self.setProperty("auto-enabled", "true");
+ self.completeCheckServerInfo(aChangeLogListener, Components.results.NS_ERROR_FAILURE);
+ }
+ function connect() {
+ // Use the async prompter to avoid multiple master password prompts
+ let promptlistener = {
+ onPromptStart: function() {
+ // Usually this function should be synchronous. The OAuth
+ // connection itself is asynchronous, but if a master
+ // password is prompted it will block on that.
+ this.onPromptAuthAvailable();
+ return true;
+ },
+
+ onPromptAuthAvailable: function() {
+ self.oauth.connect(authSuccess, authFailed, true);
+ },
+ onPromptCanceled: authFailed
+ };
+ let asyncprompter = Components.classes["@mozilla.org/messenger/msgAsyncPrompter;1"]
+ .getService(Components.interfaces.nsIMsgAsyncPrompter);
+ asyncprompter.queueAsyncAuthPrompt(self.uri.spec, false, promptlistener);
+ }
+ if (this.mUri.host == "apidata.googleusercontent.com") {
+ if (!this.oauth) {
+ let sessionId = this.id;
+ let pwMgrId = "Google CalDAV v2";
+ let authTitle = cal.calGetString("commonDialogs", "EnterUserPasswordFor2",
+ [this.name], "global");
+
+ this.oauth = new OAuth2(OAUTH_BASE_URI, OAUTH_SCOPE,
+ OAUTH_CLIENT_ID, OAUTH_HASH);
+ this.oauth.requestWindowTitle = authTitle;
+ this.oauth.requestWindowFeatures = "chrome,private,centerscreen,width=430,height=600";
+
+ Object.defineProperty(this.oauth, "refreshToken", {
+ get: function() {
+ if (!this.mRefreshToken) {
+ let pass = { value: null };
+ try {
+ let origin = "oauth:" + sessionId;
+ cal.auth.passwordManagerGet(sessionId, pass, origin, pwMgrId);
+ } catch (e) {
+ // User might have cancelled the master password prompt, thats ok
+ if (e.result != Components.results.NS_ERROR_ABORT) {
+ throw e;
+ }
+ }
+ this.mRefreshToken = pass.value;
+ }
+ return this.mRefreshToken;
+ },
+ set: function(val) {
+ try {
+ let origin = "oauth:" + sessionId;
+ if (val) {
+ cal.auth.passwordManagerSave(sessionId, val, origin, pwMgrId);
+ } else {
+ cal.auth.passwordManagerRemove(sessionId, origin, pwMgrId);
+ }
+ } catch (e) {
+ // User might have cancelled the master password prompt, thats ok
+ if (e.result != Components.results.NS_ERROR_ABORT) {
+ throw e;
+ }
+ }
+ return (this.mRefreshToken = val);
+ },
+ enumerable: true
+ });
+ }
+
+ if (this.oauth.accessToken) {
+ authSuccess();
+ } else {
+ // bug 901329: If the calendar window isn't loaded yet the
+ // master password prompt will show just the buttons and
+ // possibly hang. If we postpone until the window is loaded,
+ // all is well.
+ setTimeout(function postpone() { // eslint-disable-line func-names
+ let win = cal.getCalendarWindow();
+ if (!win || win.document.readyState != "complete") {
+ setTimeout(postpone, 0);
+ } else {
+ connect();
+ }
+ }, 0);
+ }
+ } else {
+ authSuccess();
+ }
+ },
+
+ /**
+ * Checks that the calendar URI exists and is a CalDAV calendar.
+ *
+ * setupAuthentication
+ * checkDavResourceType * You are here
+ * checkServerCaps
+ * findPrincipalNS
+ * checkPrincipalsNameSpace
+ * completeCheckServerInfo
+ */
+ checkDavResourceType: function(aChangeLogListener) {
+ this.ensureTargetCalendar();
+
+ let resourceType = kDavResourceTypeNone;
+ let self = this;
+
+ let queryXml =
+ xmlHeader +
+ '<D:propfind xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/" xmlns:C="urn:ietf:params:xml:ns:caldav">' +
+ "<D:prop>" +
+ "<D:resourcetype/>" +
+ "<D:owner/>" +
+ "<D:current-user-principal/>" +
+ "<D:supported-report-set/>" +
+ "<C:supported-calendar-component-set/>" +
+ "<CS:getctag/>" +
+ "</D:prop>" +
+ "</D:propfind>";
+
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: send: " + queryXml);
+ }
+ let streamListener = {};
+
+ streamListener.onStreamComplete = function(aLoader, aContext, aStatus, aResultLength, aResult) {
+ let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ try {
+ cal.LOG("CalDAV: Status " + request.responseStatus +
+ " on initial PROPFIND for calendar " + self.name);
+ } catch (ex) {
+ cal.LOG("CalDAV: Error without status on initial PROPFIND for calendar " +
+ self.name);
+ self.completeCheckServerInfo(aChangeLogListener,
+ Components.interfaces.calIErrors.DAV_NOT_DAV);
+ return;
+ }
+
+ let isText = true;
+
+ if ((isText || request.URI.spec != request.originalURI.spec) &&
+ self.mLastRedirectStatus == 301) {
+ // The initial PROPFIND essentially goes against the calendar
+ // collection url. If a 301 Moved Permanently redirect occurred
+ // here, we want to modify the url we use in the future.
+ let nIPS = Components.interfaces.nsIPromptService;
+
+ let promptTitle = cal.calGetString("calendar", "caldavRedirectTitle", [self.name]);
+ let promptText = cal.calGetString("calendar", "caldavRedirectText", [self.name]) +
+ "\n\n" + request.URI.spec;
+ let button1Title = cal.calGetString("calendar", "caldavRedirectDisableCalendar");
+ let flags = (nIPS.BUTTON_TITLE_YES * nIPS.BUTTON_POS_0) +
+ (nIPS.BUTTON_TITLE_IS_STRING * nIPS.BUTTON_POS_1);
+
+ let res = Services.prompt.confirmEx(cal.getCalendarWindow(),
+ promptTitle, promptText,
+ flags, null, button1Title,
+ null, null, {});
+
+ if (res == 0) { // YES
+ let newUri = request.URI;
+ cal.LOG("CalDAV: Migrating url due to redirect: " +
+ self.mUri.spec + " -> " + newUri.spec);
+ self.mUri = newUri;
+ self.setProperty("uri", newUri.spec);
+ } else if (res == 1) { // DISABLE CALENDAR
+ self.setProperty("disabled", "true");
+ self.completeCheckServerInfo(aChangeLogListener, Components.results.NS_ERROR_ABORT);
+ return;
+ }
+ }
+
+ let responseStatusCategory = Math.floor(request.responseStatus / 100);
+
+ // 4xx codes, which is either an authentication failure or
+ // something like method not allowed. This is a failure worth
+ // disabling the calendar.
+ if (responseStatusCategory == 4) {
+ self.setProperty("disabled", "true");
+ self.setProperty("auto-enabled", "true");
+ self.completeCheckServerInfo(aChangeLogListener, Components.results.NS_ERROR_ABORT);
+ return;
+ }
+
+ // 5xx codes, a server error. This could be a temporary failure,
+ // i.e a backend server being disabled.
+ if (responseStatusCategory == 5) {
+ cal.LOG("CalDAV: Server not available " + request.responseStatus +
+ ", abort sync for calendar " + self.name);
+ self.completeCheckServerInfo(aChangeLogListener, Components.results.NS_ERROR_ABORT);
+ return;
+ }
+
+ let wwwauth;
+ try {
+ wwwauth = request.getRequestHeader("Authorization");
+ self.mAuthScheme = wwwauth.split(" ")[0];
+ } catch (ex) {
+ // no auth header could mean a public calendar
+ self.mAuthScheme = "none";
+ }
+
+ if (self.mUriParams) {
+ self.mAuthScheme = "Ticket";
+ }
+ cal.LOG("CalDAV: Authentication scheme for " + self.name +
+ " is " + self.mAuthScheme);
+ // we only really need the authrealm for Digest auth
+ // since only Digest is going to time out on us
+ if (self.mAuthScheme == "Digest") {
+ let realmChop = wwwauth.split("realm=\"")[1];
+ self.mAuthRealm = realmChop.split("\", ")[0];
+ cal.LOG("CalDAV: realm " + self.mAuthRealm);
+ }
+
+ let str = cal.convertByteArray(aResult, aResultLength);
+ if (!str || request.responseStatus == 404) {
+ // No response, or the calendar no longer exists.
+ cal.LOG("CalDAV: Failed to determine resource type for" +
+ self.name);
+ self.completeCheckServerInfo(aChangeLogListener,
+ Components.interfaces.calIErrors.DAV_NOT_DAV);
+ return;
+ } else if (self.verboseLogging()) {
+ cal.LOG("CalDAV: recv: " + str);
+ }
+
+ let multistatus;
+ try {
+ multistatus = cal.xml.parseString(str);
+ } catch (ex) {
+ cal.LOG("CalDAV: Failed to determine resource type for" +
+ self.name + ": " + ex);
+ self.completeCheckServerInfo(aChangeLogListener,
+ Components.interfaces.calIErrors.DAV_NOT_DAV);
+ return;
+ }
+
+ // check for webdav-sync capability
+ // http://tools.ietf.org/html/draft-daboo-webdav-sync
+ if (caldavXPath(multistatus, "/D:multistatus/D:response/D:propstat/D:prop" +
+ "/D:supported-report-set/D:supported-report/D:report/D:sync-collection")) {
+ cal.LOG("CalDAV: Collection has webdav sync support");
+ self.mHasWebdavSyncSupport = true;
+ }
+
+ // check for server-side ctag support only if webdav sync is not available
+ let ctag = caldavXPathFirst(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/CS:getctag/text()");
+ if (!self.mHasWebdavSyncSupport && ctag) {
+ // We compare the stored ctag with the one we just got, if
+ // they don't match, we update the items in safeRefresh.
+ if (ctag == self.mCtag) {
+ self.mFirstRefreshDone = true;
+ }
+
+ self.mProposedCtag = ctag;
+ if (self.verboseLogging()) {
+ cal.LOG("CalDAV: initial ctag " + ctag + " for calendar " +
+ self.name);
+ }
+ }
+
+
+ // Use supported-calendar-component-set if the server supports it; some do not
+ // Accept name attribute from all namespaces to workaround Cosmo bug see bug 605378 comment 6
+ let supportedComponents = caldavXPath(multistatus,
+ "/D:multistatus/D:response/D:propstat/D:prop/C:supported-calendar-component-set/C:comp/@*[local-name()='name']");
+ if (supportedComponents && supportedComponents.length) {
+ self.mSupportedItemTypes = [];
+ for (compName of supportedComponents) {
+ if (self.mGenerallySupportedItemTypes.includes(compName)) {
+ self.mSupportedItemTypes.push(compName);
+ }
+ }
+ cal.LOG("Adding supported items: " + self.mSupportedItemTypes.join(",") + " for calendar: " + self.name);
+ }
+
+ // check if owner is specified; might save some work
+ let owner = caldavXPathFirst(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/D:owner/D:href/text()");
+ let cuprincipal = caldavXPathFirst(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/D:current-user-principal/D:href/text()");
+ if (cuprincipal) {
+ self.mPrincipalUrl = cuprincipal;
+ cal.LOG("CalDAV: Found principal url from DAV:current-user-principal " + self.mPrincipalUrl);
+ } else if (owner) {
+ self.mPrincipalUrl = owner;
+ cal.LOG("CalDAV: Found principal url from DAV:owner " + self.mPrincipalUrl);
+ }
+
+ let resourceTypeXml = caldavXPath(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/D:resourcetype");
+ if (!resourceTypeXml) {
+ resourceType = kDavResourceTypeNone;
+ } else if (caldavXPath(resourceTypeXml[0], "C:calendar")) {
+ resourceType = kDavResourceTypeCalendar;
+ } else if (caldavXPath(resourceTypeXml[0], "D:collection")) {
+ resourceType = kDavResourceTypeCollection;
+ }
+
+ if (resourceType == kDavResourceTypeNone) {
+ cal.LOG("CalDAV: No resource type received, " + self.name + " doesn't seem to point to a DAV resource");
+ self.completeCheckServerInfo(aChangeLogListener,
+ Components.interfaces.calIErrors.DAV_NOT_DAV);
+ return;
+ }
+
+ if (resourceType == kDavResourceTypeCollection) {
+ cal.LOG("CalDAV: " + self.name + " points to a DAV resource, but not a CalDAV calendar");
+ self.completeCheckServerInfo(aChangeLogListener,
+ Components.interfaces.calIErrors.DAV_DAV_NOT_CALDAV);
+ return;
+ }
+
+ if (resourceType == kDavResourceTypeCalendar) {
+ // If this calendar was previously offline we want to recover
+ if (self.mDisabled) {
+ self.mDisabled = false;
+ self.mReadOnly = false;
+ }
+ self.setCalHomeSet(true);
+ self.checkServerCaps(aChangeLogListener);
+ return;
+ }
+
+ // If we get here something must have gone wrong. Abort with a
+ // general error to avoid an endless loop.
+ self.completeCheckServerInfo(aChangeLogListener, Components.results.NS_ERROR_FAILURE);
+ };
+
+ this.sendHttpRequest(this.makeUri(), queryXml, MIME_TEXT_XML, null, (channel) => {
+ channel.setRequestHeader("Depth", "0", false);
+ channel.requestMethod = "PROPFIND";
+ return streamListener;
+ }, () => {
+ notifyListener(Components.results.NS_ERROR_NOT_AVAILABLE,
+ "Error preparing http channel");
+ });
+ },
+
+ /**
+ * Checks server capabilities.
+ *
+ * setupAuthentication
+ * checkDavResourceType
+ * checkServerCaps * You are here
+ * findPrincipalNS
+ * checkPrincipalsNameSpace
+ * completeCheckServerInfo
+ */
+ checkServerCaps: function(aChangeLogListener, calHomeSetUrlRetry) {
+ let homeSet = this.makeUri(null, this.mCalHomeSet);
+ let self = this;
+
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: send: OPTIONS " + homeSet.spec);
+ }
+
+ let streamListener = {};
+ streamListener.onStreamComplete = function(aLoader, aContext, aStatus, aResultLength, aResult) {
+ let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ if (request.responseStatus != 200) {
+ if (!calHomeSetUrlRetry && request.responseStatus == 404) {
+ // try again with calendar URL, see https://bugzilla.mozilla.org/show_bug.cgi?id=588799
+ cal.LOG("CalDAV: Calendar homeset was not found at parent url of calendar URL" +
+ " while querying options " + self.name + ", will try calendar URL itself now");
+ self.setCalHomeSet(false);
+ self.checkServerCaps(aChangeLogListener, true);
+ } else {
+ cal.LOG("CalDAV: Unexpected status " + request.responseStatus +
+ " while querying options " + self.name);
+ self.completeCheckServerInfo(aChangeLogListener, Components.results.NS_ERROR_FAILURE);
+ }
+
+ // No further processing needed, we have called subsequent (async) functions above.
+ return;
+ }
+
+ let dav = null;
+ try {
+ dav = request.getResponseHeader("DAV");
+ if (self.verboseLogging()) {
+ cal.LOG("CalDAV: DAV header: " + dav);
+ }
+ } catch (ex) {
+ cal.LOG("CalDAV: Error getting DAV header for " + self.name +
+ ", status " + request.responseStatus +
+ ", data: " + cal.convertByteArray(aResult, aResultLength));
+ }
+ // Google does not yet support OPTIONS but does support scheduling
+ // so we'll spoof the DAV header until Google gets fixed
+ if (self.calendarUri.host == "www.google.com") {
+ dav = "calendar-schedule";
+ // Google also reports an inbox URL distinct from the calendar
+ // URL but a) doesn't use it and b) 405s on etag queries to it
+ self.mShouldPollInbox = false;
+ }
+ if (dav && dav.includes("calendar-auto-schedule")) {
+ if (self.verboseLogging()) {
+ cal.LOG("CalDAV: Calendar " + self.name +
+ " supports calendar-auto-schedule");
+ }
+ self.hasAutoScheduling = true;
+ // leave outbound inbox/outbox scheduling off
+ } else if (dav && dav.includes("calendar-schedule")) {
+ if (self.verboseLogging()) {
+ cal.LOG("CalDAV: Calendar " + self.name +
+ " generally supports calendar-schedule");
+ }
+ self.hasScheduling = true;
+ }
+
+ if (self.hasAutoScheduling || (dav && dav.includes("calendar-schedule"))) {
+ // XXX - we really shouldn't register with the fb service
+ // if another calendar with the same principal-URL has already
+ // done so. We also shouldn't register with the fb service if we
+ // don't have an outbox.
+ if (!self.hasFreeBusy) {
+ // This may have already been set by fetchCachedMetaData,
+ // we only want to add the freebusy provider once.
+ self.hasFreeBusy = true;
+ getFreeBusyService().addProvider(self);
+ }
+ self.findPrincipalNS(aChangeLogListener);
+ } else {
+ cal.LOG("CalDAV: Server does not support CalDAV scheduling.");
+ self.completeCheckServerInfo(aChangeLogListener);
+ }
+ };
+
+ this.sendHttpRequest(homeSet, null, null, null, (channel) => {
+ channel.requestMethod = "OPTIONS";
+ return streamListener;
+ }, () => {
+ notifyListener(Components.results.NS_ERROR_NOT_AVAILABLE,
+ "Error preparing http channel");
+ });
+ },
+
+ /**
+ * Locates the principal namespace. This function should soely be called
+ * from checkServerCaps to find the principal namespace.
+ *
+ * setupAuthentication
+ * checkDavResourceType
+ * checkServerCaps
+ * findPrincipalNS * You are here
+ * checkPrincipalsNameSpace
+ * completeCheckServerInfo
+ */
+ findPrincipalNS: function(aChangeLogListener) {
+ if (this.principalUrl) {
+ // We already have a principal namespace, use it.
+ this.checkPrincipalsNameSpace([this.principalUrl],
+ aChangeLogListener);
+ return;
+ }
+
+ let homeSet = this.makeUri(null, this.mCalHomeSet);
+ let self = this;
+
+ let queryXml =
+ xmlHeader +
+ '<D:propfind xmlns:D="DAV:">' +
+ "<D:prop>" +
+ "<D:principal-collection-set/>" +
+ "</D:prop>" +
+ "</D:propfind>";
+
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: send: " + homeSet.spec + "\n" + queryXml);
+ }
+ let streamListener = {};
+ streamListener.onStreamComplete = function(aLoader, aContext, aStatus, aResultLength, aResult) {
+ let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ if (request.responseStatus != 207) {
+ cal.LOG("CalDAV: Unexpected status " + request.responseStatus +
+ " while querying principal namespace for " + self.name);
+ self.completeCheckServerInfo(aChangeLogListener,
+ Components.results.NS_ERROR_FAILURE);
+ return;
+ }
+
+ let str = cal.convertByteArray(aResult, aResultLength);
+ if (!str) {
+ cal.LOG("CalDAV: Failed to propstat principal namespace for " + self.name);
+ self.completeCheckServerInfo(aChangeLogListener,
+ Components.results.NS_ERROR_FAILURE);
+ return;
+ } else if (self.verboseLogging()) {
+ cal.LOG("CalDAV: recv: " + str);
+ }
+
+ let multistatus;
+ try {
+ multistatus = cal.xml.parseString(str);
+ } catch (ex) {
+ cal.LOG("CalDAV: Failed to propstat principal namespace for " + self.name);
+ self.completeCheckServerInfo(aChangeLogListener,
+ Components.results.NS_ERROR_FAILURE);
+ return;
+ }
+
+ let pcs = caldavXPath(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/D:principal-collection-set/D:href/text()");
+ let nsList = [];
+ if (pcs) {
+ nsList = pcs.map(x => self.ensureDecodedPath(x));
+ }
+
+ self.checkPrincipalsNameSpace(nsList, aChangeLogListener);
+ };
+
+ this.sendHttpRequest(homeSet, queryXml, MIME_TEXT_XML, null, (channel) => {
+ channel.setRequestHeader("Depth", "0", false);
+ channel.requestMethod = "PROPFIND";
+ return streamListener;
+ }, () => {
+ notifyListener(Components.results.NS_ERROR_NOT_AVAILABLE);
+ });
+ },
+
+ /**
+ * Checks the principals namespace for scheduling info. This function should
+ * soely be called from findPrincipalNS
+ *
+ * setupAuthentication
+ * checkDavResourceType
+ * checkServerCaps
+ * findPrincipalNS
+ * checkPrincipalsNameSpace * You are here
+ * completeCheckServerInfo
+ *
+ * @param aNameSpaceList List of available namespaces
+ */
+ checkPrincipalsNameSpace: function(aNameSpaceList, aChangeLogListener) {
+ let self = this;
+ let doesntSupportScheduling = () => {
+ this.hasScheduling = false;
+ this.mInboxUrl = null;
+ this.mOutboxUrl = null;
+ this.completeCheckServerInfo(aChangeLogListener);
+ };
+
+ if (!aNameSpaceList.length) {
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: principal namespace list empty, calendar " +
+ this.name + " doesn't support scheduling");
+ }
+ doesntSupportScheduling();
+ return;
+ }
+
+ // Remove trailing slash, if its there
+ let homePath = this.ensureEncodedPath(this.mCalHomeSet.spec.replace(/\/$/, ""));
+ let queryXml, queryMethod, queryDepth;
+ if (this.mPrincipalUrl) {
+ queryXml =
+ xmlHeader +
+ '<D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">' +
+ "<D:prop>" +
+ "<C:calendar-home-set/>" +
+ "<C:calendar-user-address-set/>" +
+ "<C:schedule-inbox-URL/>" +
+ "<C:schedule-outbox-URL/>" +
+ "</D:prop>" +
+ "</D:propfind>";
+ queryMethod = "PROPFIND";
+ queryDepth = 0;
+ } else {
+ queryXml =
+ xmlHeader +
+ '<D:principal-property-search xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">' +
+ "<D:property-search>" +
+ "<D:prop>" +
+ "<C:calendar-home-set/>" +
+ "</D:prop>" +
+ "<D:match>" + cal.xml.escapeString(homePath) + "</D:match>" +
+ "</D:property-search>" +
+ "<D:prop>" +
+ "<C:calendar-home-set/>" +
+ "<C:calendar-user-address-set/>" +
+ "<C:schedule-inbox-URL/>" +
+ "<C:schedule-outbox-URL/>" +
+ "</D:prop>" +
+ "</D:principal-property-search>";
+ queryMethod = "REPORT";
+ queryDepth = 1;
+ }
+
+ // We want a trailing slash, ensure it.
+ let nextNS = aNameSpaceList.pop().replace(/([^\/])$/, "$1/");
+ let requestUri = makeURL(this.calendarUri.prePath + this.ensureEncodedPath(nextNS));
+
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: send: " + queryMethod + " " + requestUri.spec + "\n" + queryXml);
+ }
+
+ let streamListener = {};
+ streamListener.onStreamComplete = function(aLoader, aContext, aStatus, aResultLength, aResult) {
+ let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ let str = cal.convertByteArray(aResult, aResultLength);
+ if (!str) {
+ cal.LOG("CalDAV: Failed to report principals namespace for " + self.name);
+ doesntSupportScheduling();
+ return;
+ } else if (self.verboseLogging()) {
+ cal.LOG("CalDAV: recv: " + str);
+ }
+
+ if (request.responseStatus != 207) {
+ cal.LOG("CalDAV: Bad response to in/outbox query, status " +
+ request.responseStatus);
+ doesntSupportScheduling();
+ return;
+ }
+
+ let multistatus;
+ try {
+ multistatus = cal.xml.parseString(str);
+ } catch (ex) {
+ cal.LOG("CalDAV: Could not parse multistatus response: " + ex + "\n" + str);
+ doesntSupportScheduling();
+ return;
+ }
+
+ let homeSets = caldavXPath(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/C:calendar-home-set/D:href/text()");
+ function homeSetMatches(homeSet) {
+ let normalized = homeSet.replace(/([^\/])$/, "$1/");
+ let chs = self.mCalHomeSet;
+ return normalized == chs.path || normalized == chs.spec;
+ }
+ function createBoxUrl(path) {
+ let url = self.mUri.clone();
+ url.path = self.ensureDecodedPath(path);
+ // Make sure the uri has a / at the end, as we do with the calendarUri.
+ if (url.path.charAt(url.path.length - 1) != "/") {
+ url.path += "/";
+ }
+ return url;
+ }
+
+ // If there are multiple home sets, we need to match the email addresses for scheduling.
+ // If there is only one, assume its the right one.
+ // TODO with multiple address sets, we should just use the ACL manager.
+ if (homeSets && (homeSets.length == 1 || homeSets.some(homeSetMatches))) {
+ let cuaSets = caldavXPath(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/C:calendar-user-address-set/D:href/text()");
+ if (cuaSets) {
+ for (let addr of cuaSets) {
+ if (addr.match(/^mailto:/i)) {
+ self.mCalendarUserAddress = addr;
+ }
+ }
+ }
+
+
+ let inboxPath = caldavXPathFirst(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/C:schedule-inbox-URL/D:href/text()");
+ if (!inboxPath) {
+ // most likely this is a Kerio server that omits the "href"
+ inboxPath = caldavXPathFirst(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/C:schedule-inbox-URL/text()");
+ }
+ self.mInboxUrl = createBoxUrl(inboxPath);
+
+ if (self.calendarUri.spec == self.mInboxUrl.spec) {
+ // If the inbox matches the calendar uri (i.e SOGo), then we
+ // don't need to poll the inbox.
+ self.mShouldPollInbox = false;
+ }
+
+ let outboxPath = caldavXPathFirst(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/C:schedule-outbox-URL/D:href/text()");
+ if (!outboxPath) {
+ // most likely this is a Kerio server that omits the "href"
+ outboxPath = caldavXPathFirst(multistatus, "/D:multistatus/D:response/D:propstat/D:prop/C:schedule-outbox-URL/text()");
+ }
+ self.mOutboxUrl = createBoxUrl(outboxPath);
+ }
+
+ if (!self.calendarUserAddress ||
+ !self.mInboxUrl ||
+ !self.mOutboxUrl) {
+ if (aNameSpaceList.length) {
+ // Check the next namespace to find the info we need.
+ self.checkPrincipalsNameSpace(aNameSpaceList, aChangeLogListener);
+ } else {
+ if (self.verboseLogging()) {
+ cal.LOG("CalDAV: principal namespace list empty, calendar " +
+ self.name + " doesn't support scheduling");
+ }
+ doesntSupportScheduling();
+ }
+ } else {
+ // We have everything, complete.
+ self.completeCheckServerInfo(aChangeLogListener);
+ }
+ };
+ this.sendHttpRequest(requestUri, queryXml, MIME_TEXT_XML, null, (channel) => {
+ if (queryDepth == 0) {
+ // Set header, doing this for Depth: 1 is not needed since thats the
+ // default.
+ channel.setRequestHeader("Depth", "0", false);
+ }
+ channel.requestMethod = queryMethod;
+ return streamListener;
+ }, () => {
+ notifyListener(Components.results.NS_ERROR_NOT_AVAILABLE);
+ });
+ },
+
+ /**
+ * This is called to complete checking the server info. It should be the
+ * final call when checking server options. This will either report the
+ * error or if it is a success then refresh the calendar.
+ *
+ * setupAuthentication
+ * checkDavResourceType
+ * checkServerCaps
+ * findPrincipalNS
+ * checkPrincipalsNameSpace
+ * completeCheckServerInfo * You are here
+ */
+ completeCheckServerInfo: function(aChangeLogListener, aError) {
+ if (Components.isSuccessCode(aError)) {
+ // "undefined" is a successcode, so all is good
+ this.saveCalendarProperties();
+ this.checkedServerInfo = true;
+ this.setProperty("currentStatus", Components.results.NS_OK);
+
+ if (this.isCached) {
+ this.safeRefresh(aChangeLogListener);
+ } else {
+ this.refresh();
+ }
+ } else {
+ this.reportDavError(aError);
+ if (this.isCached && aChangeLogListener) {
+ aChangeLogListener.onResult({ status: Components.results.NS_ERROR_FAILURE },
+ Components.results.NS_ERROR_FAILURE);
+ }
+ }
+ },
+
+ /**
+ * Called to report a certain DAV error. Strings and modification type are
+ * handled here.
+ */
+ reportDavError: function(aErrNo, status, extraInfo) {
+ let mapError = {};
+ mapError[Components.interfaces.calIErrors.DAV_NOT_DAV] = "dav_notDav";
+ mapError[Components.interfaces.calIErrors.DAV_DAV_NOT_CALDAV] = "dav_davNotCaldav";
+ mapError[Components.interfaces.calIErrors.DAV_PUT_ERROR] = "itemPutError";
+ mapError[Components.interfaces.calIErrors.DAV_REMOVE_ERROR] = "itemDeleteError";
+ mapError[Components.interfaces.calIErrors.DAV_REPORT_ERROR] = "disabledMode";
+
+ let mapModification = {};
+ mapModification[Components.interfaces.calIErrors.DAV_NOT_DAV] = false;
+ mapModification[Components.interfaces.calIErrors.DAV_DAV_NOT_CALDAV] = false;
+ mapModification[Components.interfaces.calIErrors.DAV_PUT_ERROR] = true;
+ mapModification[Components.interfaces.calIErrors.DAV_REMOVE_ERROR] = true;
+ mapModification[Components.interfaces.calIErrors.DAV_REPORT_ERROR] = false;
+
+ let message = mapError[aErrNo];
+ let localizedMessage;
+ let modificationError = mapModification[aErrNo];
+
+ if (!message) {
+ // Only notify if there is a message for this error
+ return;
+ }
+ localizedMessage = cal.calGetString("calendar", message, [this.mUri.spec]);
+ this.mReadOnly = true;
+ this.mDisabled = true;
+ this.notifyError(aErrNo, localizedMessage);
+ this.notifyError(modificationError
+ ? Components.interfaces.calIErrors.MODIFICATION_FAILED
+ : Components.interfaces.calIErrors.READ_FAILED,
+ this.buildDetailedMessage(status, extraInfo));
+ },
+
+ buildDetailedMessage: function(status, extraInfo) {
+ if (!status) {
+ return "";
+ }
+
+ let props = Services.strings.createBundle("chrome://calendar/locale/calendar.properties");
+ let statusString;
+ try {
+ statusString = props.GetStringFromName("caldavRequestStatusCodeString" + status);
+ } catch (e) {
+ // Fallback on generic string if no string is defined for the status code
+ statusString = props.GetStringFromName("caldavRequestStatusCodeStringGeneric");
+ }
+ return props.formatStringFromName("caldavRequestStatusCode", [status], 1) + ", " +
+ statusString + "\n\n" +
+ (extraInfo ? extraInfo : "");
+ },
+
+ //
+ // calIFreeBusyProvider interface
+ //
+
+ getFreeBusyIntervals: function(aCalId, aRangeStart, aRangeEnd, aBusyTypes, aListener) {
+ // We explicitly don't check for hasScheduling here to allow free-busy queries
+ // even in case sched is turned off.
+ if (!this.outboxUrl || !this.calendarUserAddress) {
+ cal.LOG("CalDAV: Calendar " + this.name + " doen't support scheduling;" +
+ " freebusy query not possible");
+ aListener.onResult(null, null);
+ return;
+ }
+
+ if (!this.firstInRealm()) {
+ // don't spam every known outbox with freebusy queries
+ aListener.onResult(null, null);
+ return;
+ }
+
+ // We tweak the organizer lookup here: If e.g. scheduling is turned off, then the
+ // configured email takes place being the organizerId for scheduling which need
+ // not match against the calendar-user-address:
+ let orgId = this.getProperty("organizerId");
+ if (orgId && orgId.toLowerCase() == aCalId.toLowerCase()) {
+ aCalId = this.calendarUserAddress; // continue with calendar-user-address
+ }
+
+ // the caller prepends MAILTO: to calid strings containing @
+ // but apple needs that to be mailto:
+ let aCalIdParts = aCalId.split(":");
+ aCalIdParts[0] = aCalIdParts[0].toLowerCase();
+
+ if (aCalIdParts[0] != "mailto" &&
+ aCalIdParts[0] != "http" &&
+ aCalIdParts[0] != "https") {
+ aListener.onResult(null, null);
+ return;
+ }
+ let mailto_aCalId = aCalIdParts.join(":");
+
+ let self = this;
+
+ let organizer = this.calendarUserAddress;
+
+ let fbQuery = getIcsService().createIcalComponent("VCALENDAR");
+ calSetProdidVersion(fbQuery);
+ let prop = getIcsService().createIcalProperty("METHOD");
+ prop.value = "REQUEST";
+ fbQuery.addProperty(prop);
+ let fbComp = getIcsService().createIcalComponent("VFREEBUSY");
+ fbComp.stampTime = now().getInTimezone(UTC());
+ prop = getIcsService().createIcalProperty("ORGANIZER");
+ prop.value = organizer;
+ fbComp.addProperty(prop);
+ fbComp.startTime = aRangeStart.getInTimezone(UTC());
+ fbComp.endTime = aRangeEnd.getInTimezone(UTC());
+ fbComp.uid = cal.getUUID();
+ prop = getIcsService().createIcalProperty("ATTENDEE");
+ prop.setParameter("PARTSTAT", "NEEDS-ACTION");
+ prop.setParameter("ROLE", "REQ-PARTICIPANT");
+ prop.setParameter("CUTYPE", "INDIVIDUAL");
+ prop.value = mailto_aCalId;
+ fbComp.addProperty(prop);
+ fbQuery.addSubcomponent(fbComp);
+ fbQuery = fbQuery.serializeToICS();
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: send (Originator=" + organizer +
+ ",Recipient=" + mailto_aCalId + "): " + fbQuery);
+ }
+
+ let streamListener = {};
+
+ streamListener.onStreamComplete = function(aLoader, aContext, aStatus,
+ aResultLength, aResult) {
+ let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ let str = cal.convertByteArray(aResult, aResultLength);
+ if (!str) {
+ cal.LOG("CalDAV: Failed to parse freebusy response from " + self.name);
+ } else if (self.verboseLogging()) {
+ cal.LOG("CalDAV: recv: " + str);
+ }
+
+ if (request.responseStatus == 200) {
+ let periodsToReturn = [];
+ let fbTypeMap = {};
+ fbTypeMap.FREE = calIFreeBusyInterval.FREE;
+ fbTypeMap.BUSY = calIFreeBusyInterval.BUSY;
+ fbTypeMap["BUSY-UNAVAILABLE"] = calIFreeBusyInterval.BUSY_UNAVAILABLE;
+ fbTypeMap["BUSY-TENTATIVE"] = calIFreeBusyInterval.BUSY_TENTATIVE;
+
+ let fbResult;
+ try {
+ fbResult = cal.xml.parseString(str);
+ } catch (ex) {
+ cal.LOG("CalDAV: Could not parse freebusy response " + ex);
+ aListener.onResult(null, null);
+ return;
+ }
+
+ let status = caldavXPathFirst(fbResult, "/C:schedule-response/C:response/C:request-status/text()");
+ if (!status || status.substr(0, 1) != "2") {
+ cal.LOG("CalDAV: Got status " + status + " in response to " +
+ "freebusy query for " + self.name);
+ aListener.onResult(null, null);
+ return;
+ }
+ if (status.substr(0, 3) != "2.0") {
+ cal.LOG("CalDAV: Got status " + status + " in response to " +
+ "freebusy query for" + self.name);
+ }
+
+ let caldata = caldavXPathFirst(fbResult, "/C:schedule-response/C:response/C:calendar-data/text()");
+ try {
+ let calComp = cal.getIcsService().parseICS(caldata, null);
+ for (let calFbComp of cal.ical.calendarComponentIterator(calComp)) {
+ let interval;
+
+ let replyRangeStart = calFbComp.startTime;
+ if (replyRangeStart && (aRangeStart.compare(replyRangeStart) == -1)) {
+ interval = new cal.FreeBusyInterval(aCalId,
+ calIFreeBusyInterval.UNKNOWN,
+ aRangeStart,
+ replyRangeStart);
+ periodsToReturn.push(interval);
+ }
+ let replyRangeEnd = calFbComp.endTime;
+ if (replyRangeEnd && (aRangeEnd.compare(replyRangeEnd) == 1)) {
+ interval = new cal.FreeBusyInterval(aCalId,
+ calIFreeBusyInterval.UNKNOWN,
+ replyRangeEnd,
+ aRangeEnd);
+ periodsToReturn.push(interval);
+ }
+
+ for (let fbProp of cal.ical.propertyIterator(calFbComp, "FREEBUSY")) {
+ let fbType = fbProp.getParameter("FBTYPE");
+ if (fbType) {
+ fbType = fbTypeMap[fbType];
+ } else {
+ fbType = calIFreeBusyInterval.BUSY;
+ }
+ let parts = fbProp.value.split("/");
+ let begin = cal.createDateTime(parts[0]);
+ let end;
+ if (parts[1].charAt(0) == "P") { // this is a duration
+ end = begin.clone();
+ end.addDuration(cal.createDuration(parts[1]));
+ } else {
+ // This is a date string
+ end = cal.createDateTime(parts[1]);
+ }
+ interval = new cal.FreeBusyInterval(aCalId,
+ fbType,
+ begin,
+ end);
+ periodsToReturn.push(interval);
+ }
+ }
+ } catch (exc) {
+ cal.ERROR("CalDAV: Error parsing free-busy info.");
+ }
+
+ aListener.onResult(null, periodsToReturn);
+ } else {
+ cal.LOG("CalDAV: Received status " + request.responseStatus +
+ " from freebusy query for " + self.name);
+ aListener.onResult(null, null);
+ }
+ };
+
+ let fbUri = this.makeUri(null, this.outboxUrl);
+ this.sendHttpRequest(fbUri, fbQuery, MIME_TEXT_CALENDAR, null, (channel) => {
+ channel.requestMethod = "POST";
+ channel.setRequestHeader("Originator", organizer, false);
+ channel.setRequestHeader("Recipient", mailto_aCalId, false);
+ return streamListener;
+ }, () => {
+ notifyListener(Components.results.NS_ERROR_NOT_AVAILABLE,
+ "Error preparing http channel");
+ });
+ },
+
+ /**
+ * Extract the path from the full spec, if the regexp failed, log
+ * warning and return unaltered path.
+ */
+ extractPathFromSpec: function(aSpec) {
+ // The parsed array should look like this:
+ // a[0] = full string
+ // a[1] = scheme
+ // a[2] = everything between the scheme and the start of the path
+ // a[3] = extracted path
+ let a = aSpec.match("(https?)(://[^/]*)([^#?]*)");
+ if (a && a[3]) {
+ return a[3];
+ }
+ cal.WARN("CalDAV: Spec could not be parsed, returning as-is: " + aSpec);
+ return aSpec;
+ },
+ /**
+ * This is called to create an encoded path from a unencoded path OR
+ * encoded full url
+ *
+ * @param aString {string} un-encoded path OR encoded uri spec.
+ */
+ ensureEncodedPath: function(aString) {
+ if (aString.charAt(0) != "/") {
+ aString = this.ensureDecodedPath(aString);
+ }
+ let uriComponents = aString.split("/");
+ uriComponents = uriComponents.map(encodeURIComponent);
+ return uriComponents.join("/");
+ },
+
+ /**
+ * This is called to get a decoded path from an encoded path or uri spec.
+ *
+ * @param aString {string} Represents either a path
+ * or a full uri that needs to be decoded.
+ */
+ ensureDecodedPath: function(aString) {
+ if (aString.charAt(0) != "/") {
+ aString = this.extractPathFromSpec(aString);
+ }
+
+ let uriComponents = aString.split("/");
+ for (let i = 0; i < uriComponents.length; i++) {
+ try {
+ uriComponents[i] = decodeURIComponent(uriComponents[i]);
+ } catch (e) {
+ cal.WARN("CalDAV: Exception decoding path " + aString + ", segment: " + uriComponents[i]);
+ }
+ }
+ return uriComponents.join("/");
+ },
+ isInbox: function(aString) {
+ // Note: If you change this, make sure it really returns a boolean
+ // value and not null!
+ return (this.hasScheduling || this.hasAutoScheduling) &&
+ this.mInboxUrl != null &&
+ aString.startsWith(this.mInboxUrl.spec);
+ },
+
+ /**
+ * Query contents of scheduling inbox
+ *
+ */
+ pollInbox: function() {
+ // If polling the inbox was switched off, no need to poll the inbox.
+ // Also, if we have more than one calendar in this CalDAV account, we
+ // want only one of them to be checking the inbox.
+ if ((!this.hasScheduling && !this.hasAutoScheduling) || !this.mShouldPollInbox || !this.firstInRealm()) {
+ return;
+ }
+
+ this.getUpdatedItems(this.mInboxUrl, null);
+ },
+
+ //
+ // take calISchedulingSupport interface base implementation (cal.ProviderBase)
+ //
+
+ processItipReply: function(aItem, aPath) {
+ // modify partstat for in-calendar item
+ // delete item from inbox
+ let self = this;
+
+ let getItemListener = {};
+ getItemListener.QueryInterface = XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]);
+ getItemListener.onOperationComplete = function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ };
+ getItemListener.onGetResult = function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ let itemToUpdate = aItems[0];
+ if (aItem.recurrenceId && itemToUpdate.recurrenceInfo) {
+ itemToUpdate = itemToUpdate.recurrenceInfo.getOccurrenceFor(aItem.recurrenceId);
+ }
+ let newItem = itemToUpdate.clone();
+
+ for (let attendee of aItem.getAttendees({})) {
+ let att = newItem.getAttendeeById(attendee.id);
+ if (att) {
+ newItem.removeAttendee(att);
+ att = att.clone();
+ att.participationStatus = attendee.participationStatus;
+ newItem.addAttendee(att);
+ }
+ }
+ self.doModifyItem(newItem, itemToUpdate.parentItem /* related to bug 396182 */,
+ modListener, true);
+ };
+
+ let modListener = {};
+ modListener.QueryInterface = XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]);
+ modListener.onOperationComplete = function(aCalendar, aStatus, aOperationType, aItemId, aDetail) {
+ cal.LOG("CalDAV: status " + aStatus + " while processing iTIP REPLY " +
+ " for " + self.name);
+ // don't delete the REPLY item from inbox unless modifying the master
+ // item was successful
+ if (aStatus == 0) { // aStatus undocumented; 0 seems to indicate no error
+ let delUri = self.calendarUri.clone();
+ delUri.path = self.ensureEncodedPath(aPath);
+ self.doDeleteItem(aItem, null, true, true, delUri);
+ }
+ };
+
+ this.mOfflineStorage.getItem(aItem.id, getItemListener);
+ },
+
+ canNotify: function(aMethod, aItem) {
+ if (this.hasAutoScheduling) {
+ // canNotify should return false if the schedule agent is client
+ // so the itip transport(imip) takes care of notifying participants
+ if (aItem.organizer &&
+ aItem.organizer.getProperty("SCHEDULE-AGENT") == "CLIENT") {
+ return false;
+ }
+ return true;
+ }
+ return false; // use outbound iTIP for all
+ },
+
+ //
+ // calIItipTransport interface
+ //
+
+ get scheme() {
+ return "mailto";
+ },
+
+ mSenderAddress: null,
+ get senderAddress() {
+ return this.mSenderAddress || this.calendarUserAddress;
+ },
+ set senderAddress(aString) {
+ return (this.mSenderAddress = aString);
+ },
+
+ sendItems: function(aCount, aRecipients, aItipItem) {
+ if (this.hasAutoScheduling) {
+ // If auto scheduling is supported by the server we still need
+ // to send out REPLIES for meetings where the ORGANIZER has the
+ // parameter SCHEDULE-AGENT set to CLIENT, this property is
+ // checked in in canNotify()
+ if (aItipItem.responseMethod == "REPLY") {
+ let imipTransport = cal.getImipTransport(this);
+ if (imipTransport) {
+ imipTransport.sendItems(aCount, aRecipients, aItipItem);
+ }
+ }
+ // Servers supporting auto schedule should handle all other
+ // scheduling operations for now. Note that eventually the client
+ // could support setting a SCHEDULE-AGENT=CLIENT parameter on
+ // ATTENDEES and/or interpreting the SCHEDULE-STATUS parameter which
+ // could translate in the client sending out IMIP REQUESTS
+ // for specific attendees.
+ return false;
+ }
+
+ if (aItipItem.responseMethod == "REPLY") {
+ // Get my participation status
+ let attendee = aItipItem.getItemList({})[0].getAttendeeById(this.calendarUserAddress);
+ if (!attendee) {
+ return false;
+ }
+ // work around BUG 351589, the below just removes RSVP:
+ aItipItem.setAttendeeStatus(attendee.id, attendee.participationStatus);
+ }
+
+ for (let item of aItipItem.getItemList({})) {
+ let serializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"]
+ .createInstance(Components.interfaces.calIIcsSerializer);
+ serializer.addItems([item], 1);
+ let methodProp = getIcsService().createIcalProperty("METHOD");
+ methodProp.value = aItipItem.responseMethod;
+ serializer.addProperty(methodProp);
+
+ let self = this;
+ let streamListener = {
+ onStreamComplete: function(aLoader, aContext, aStatus, aResultLength, aResult) {
+ let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ let status;
+ try {
+ status = request.responseStatus;
+ } catch (ex) {
+ status = Components.interfaces.calIErrors.DAV_POST_ERROR;
+ cal.LOG("CalDAV: no response status when sending iTIP for" +
+ self.name);
+ }
+
+ if (status != 200) {
+ cal.LOG("CalDAV: Sending iTIP failed with status " + status +
+ " for " + self.name);
+ }
+
+ let str = cal.convertByteArray(aResult, aResultLength, "UTF-8", false);
+ if (str) {
+ if (self.verboseLogging()) {
+ cal.LOG("CalDAV: recv: " + str);
+ }
+ } else {
+ cal.LOG("CalDAV: Failed to parse iTIP response for" +
+ self.name);
+ }
+
+ let responseXML;
+ try {
+ responseXML = cal.xml.parseString(str);
+ } catch (ex) {
+ cal.LOG("CalDAV: Could not parse multistatus response: " + ex + "\n" + str);
+ return;
+ }
+
+ let remainingAttendees = [];
+ // TODO The following XPath expressions are currently
+ // untested code, as I don't have a caldav-sched server
+ // available. If you find someone who does, please test!
+ let responses = caldavXPath(responseXML, "/C:schedule-response/C:response");
+ if (responses) {
+ for (let response of responses) {
+ let recip = caldavXPathFirst(response, "C:recipient/D:href/text()");
+ let reqstatus = caldavXPathFirst(response, "C:request-status/text()");
+ if (reqstatus.substr(0, 1) != "2") {
+ if (self.verboseLogging()) {
+ cal.LOG("CalDAV: Failed scheduling delivery to " + recip);
+ }
+ for (let att of aRecipients) {
+ if (att.id.toLowerCase() == recip.toLowerCase()) {
+ remainingAttendees.push(att);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (remainingAttendees.length) {
+ // try to fall back to email delivery if CalDAV-sched
+ // didn't work
+ let imipTransport = cal.getImipTransport(self);
+ if (imipTransport) {
+ if (self.verboseLogging()) {
+ cal.LOG("CalDAV: sending email to " + remainingAttendees.length + " recipients");
+ }
+ imipTransport.sendItems(remainingAttendees.length, remainingAttendees, aItipItem);
+ } else {
+ cal.LOG("CalDAV: no fallback to iTIP/iMIP transport for " +
+ self.name);
+ }
+ }
+ }
+ };
+
+ let uploadData = serializer.serializeToString();
+ let requestUri = this.makeUri(null, this.outboxUrl);
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: send(" + requestUri.spec + "): " + uploadData);
+ }
+ this.sendHttpRequest(requestUri, uploadData, MIME_TEXT_CALENDAR, null, (channel) => {
+ channel.requestMethod = "POST";
+ channel.setRequestHeader("Originator", this.calendarUserAddress, false);
+ for (let recipient of aRecipients) {
+ channel.setRequestHeader("Recipient", recipient.id, true);
+ }
+ return streamListener;
+ }, () => {
+ notifyListener(Components.results.NS_ERROR_NOT_AVAILABLE,
+ "Error preparing http channel");
+ });
+ }
+ return true;
+ },
+
+ mVerboseLogging: undefined,
+ verboseLogging: function() {
+ if (this.mVerboseLogging === undefined) {
+ this.mVerboseLogging = Preferences.get("calendar.debug.log.verbose", false);
+ }
+ return this.mVerboseLogging;
+ },
+
+ getSerializedItem: function(aItem) {
+ let serializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"]
+ .createInstance(Components.interfaces.calIIcsSerializer);
+ serializer.addItems([aItem], 1);
+ let serializedItem = serializer.serializeToString();
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: send: " + serializedItem);
+ }
+ return serializedItem;
+ },
+
+ // nsIChannelEventSink implementation
+ asyncOnChannelRedirect: function(aOldChannel, aNewChannel, aFlags, aCallback) {
+ let uploadData;
+ let uploadContent;
+ if (aOldChannel instanceof Components.interfaces.nsIUploadChannel &&
+ aOldChannel instanceof Components.interfaces.nsIHttpChannel &&
+ aOldChannel.uploadStream) {
+ uploadData = aOldChannel.uploadStream;
+ uploadContent = aOldChannel.getRequestHeader("Content-Type");
+ }
+
+ cal.prepHttpChannel(null,
+ uploadData,
+ uploadContent,
+ this,
+ aNewChannel);
+
+ // Make sure we can get/set headers on both channels.
+ aNewChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
+ aOldChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
+
+ try {
+ this.mLastRedirectStatus = aOldChannel.responseStatus;
+ } catch (e) {
+ this.mLastRedirectStatus = null;
+ }
+
+ function copyHeader(aHdr) {
+ try {
+ let hdrValue = aOldChannel.getRequestHeader(aHdr);
+ if (hdrValue) {
+ aNewChannel.setRequestHeader(aHdr, hdrValue, false);
+ }
+ } catch (e) {
+ if (e.code != Components.results.NS_ERROR_NOT_AVAILIBLE) {
+ // The header could possibly not be availible, ignore that
+ // case but throw otherwise
+ throw e;
+ }
+ }
+ }
+
+ // If any other header is used, it should be added here. We might want
+ // to just copy all headers over to the new channel.
+ copyHeader("Depth");
+ copyHeader("Originator");
+ copyHeader("Recipient");
+ copyHeader("If-None-Match");
+ copyHeader("If-Match");
+ if (aNewChannel.URI.host == "apidata.googleusercontent.com") {
+ copyHeader("Authorization");
+ }
+
+ aNewChannel.requestMethod = aOldChannel.requestMethod;
+
+ aCallback.onRedirectVerifyCallback(Components.results.NS_OK);
+ }
+};
+
+function calDavObserver(aCalendar) {
+ this.mCalendar = aCalendar;
+}
+
+// Before you spend time trying to find out what this means, please note that
+// doing so and using the information WILL cause Google to revoke Lightning's
+// privileges, which means not one Lightning user will be able to connect to
+// Google Calendar via CalDAV. This will cause unhappy users all around which
+// means that the Lightning developers will have to spend more time with user
+// support, which means less time for features, releases and bugfixes. For a
+// paid developer this would actually mean financial harm.
+//
+// Do you really want all of this to be your fault? Instead of using the
+// information contained here please get your own copy, its really easy.
+/* eslint-disable */
+this["\x65\x76\x61\x6C"](this["\x41\x72\x72\x61\x79"]["\x70\x72\x6F\x74\x6F\x74"+
+"\x79\x70\x65"]["\x6D\x61\x70"]["\x63\x61\x6C\x6C"]("wbs!!!PBVUI`CBTF`VSJ!>!#iu"+
+"uqt;00bddpvout/hpphmf/dpn0p0#<wbs!PBVUI`TDPQF!>!#iuuqt;00xxx/hpphmfbqjt/dpn0bv"+
+"ui0dbmfoebs#<wbs!PBVUI`DMJFOU`JE!>!#831674:95649/bqqt/hpphmfvtfsdpoufou/dpn#<w"+
+"bs!PBVUI`IBTI!>!#zVs7YVgyvsbguj7s8{1TTfJR#<",function(_){return this["\x53\x74"+
+"\x72\x69\x6E\x67"]["\x66\x72\x6F\x6D\x43\x68\x61\x72\x43\x6F\x64\x65"](_["\x63"+
+"\x68\x61\x72\x43\x6F\x64\x65\x41\x74"](0)-1)},this)["\x6A\x6F\x69\x6E"](""));
+/* eslint-enable */
+
+calDavObserver.prototype = {
+ mCalendar: null,
+ mInBatch: false,
+
+ // calIObserver:
+ onStartBatch: function() {
+ this.mCalendar.observers.notify("onStartBatch");
+ this.mInBatch = true;
+ },
+ onEndBatch: function() {
+ this.mCalendar.observers.notify("onEndBatch");
+ this.mInBatch = false;
+ },
+ onLoad: function(calendar) {
+ this.mCalendar.observers.notify("onLoad", [calendar]);
+ },
+ onAddItem: function(aItem) {
+ this.mCalendar.observers.notify("onAddItem", [aItem]);
+ },
+ onModifyItem: function(aNewItem, aOldItem) {
+ this.mCalendar.observers.notify("onModifyItem", [aNewItem, aOldItem]);
+ },
+ onDeleteItem: function(aDeletedItem) {
+ this.mCalendar.observers.notify("onDeleteItem", [aDeletedItem]);
+ },
+ onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
+ this.mCalendar.observers.notify("onPropertyChanged", [aCalendar, aName, aValue, aOldValue]);
+ },
+ onPropertyDeleting: function(aCalendar, aName) {
+ this.mCalendar.observers.notify("onPropertyDeleting", [aCalendar, aName]);
+ },
+
+ onError: function(aCalendar, aErrNo, aMessage) {
+ this.mCalendar.readOnly = true;
+ this.mCalendar.notifyError(aErrNo, aMessage);
+ }
+};
+
+/** Module Registration */
+var scriptLoadOrder = [
+ "calUtils.js",
+ "calDavRequestHandlers.js"
+];
+
+this.NSGetFactory = cal.loadingNSGetFactory(scriptLoadOrder, [calDavCalendar], this);