diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/calendar/providers/caldav/modules/CalDavRequestHandlers.jsm | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/calendar/providers/caldav/modules/CalDavRequestHandlers.jsm')
-rw-r--r-- | comm/calendar/providers/caldav/modules/CalDavRequestHandlers.jsm | 1091 |
1 files changed, 1091 insertions, 0 deletions
diff --git a/comm/calendar/providers/caldav/modules/CalDavRequestHandlers.jsm b/comm/calendar/providers/caldav/modules/CalDavRequestHandlers.jsm new file mode 100644 index 0000000000..c5055d1a1f --- /dev/null +++ b/comm/calendar/providers/caldav/modules/CalDavRequestHandlers.jsm @@ -0,0 +1,1091 @@ +/* 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/. */ + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +var { CalDavLegacySAXRequest } = ChromeUtils.import("resource:///modules/caldav/CalDavRequest.jsm"); + +/* exported CalDavEtagsHandler, CalDavWebDavSyncHandler, CalDavMultigetSyncHandler */ + +const EXPORTED_SYMBOLS = [ + "CalDavEtagsHandler", + "CalDavWebDavSyncHandler", + "CalDavMultigetSyncHandler", +]; + +const XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n'; +const MIME_TEXT_XML = "text/xml; charset=utf-8"; + +/** + * Accumulate all XML response, then parse with DOMParser. This class imitates + * nsISAXXMLReader by calling startDocument/endDocument and startElement/endElement. + */ +class XMLResponseHandler { + constructor() { + this._inStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + this._xmlString = ""; + } + + /** + * @see nsIStreamListener + */ + onDataAvailable(request, inputStream, offset, count) { + this._inStream.init(inputStream); + // What we get from inputStream is BinaryString, decode it to UTF-8. + this._xmlString += new TextDecoder("UTF-8").decode( + this._binaryStringToTypedArray(this._inStream.read(count)) + ); + } + + /** + * Log the response code and body. + * + * @param {number} responseStatus + */ + logResponse(responseStatus) { + if (this.calendar.verboseLogging()) { + cal.LOG(`CalDAV: recv (${responseStatus}): ${this._xmlString}`); + } + } + + /** + * Parse this._xmlString with DOMParser, then create a TreeWalker and start + * walking the node tree. + */ + async handleResponse() { + let parser = new DOMParser(); + let doc; + try { + doc = parser.parseFromString(this._xmlString, "application/xml"); + } catch (e) { + cal.ERROR("CALDAV: DOMParser parse error: ", e); + this.fatalError(); + } + + let treeWalker = doc.createTreeWalker(doc.documentElement, NodeFilter.SHOW_ELEMENT); + this.startDocument(); + await this._walk(treeWalker); + await this.endDocument(); + } + + /** + * Reset this._xmlString. + */ + resetXMLResponseHandler() { + this._xmlString = ""; + } + + /** + * Converts a binary string into a Uint8Array. + * + * @param {BinaryString} str - The string to convert. + * @returns {Uint8Array}. + */ + _binaryStringToTypedArray(str) { + let arr = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + arr[i] = str.charCodeAt(i); + } + return arr; + } + + /** + * Walk the tree node by node, call startElement and endElement when appropriate. + */ + async _walk(treeWalker) { + let currentNode = treeWalker.currentNode; + if (currentNode) { + this.startElement("", currentNode.localName, currentNode.nodeName, ""); + + // Traverse children first. + let firstChild = treeWalker.firstChild(); + if (firstChild) { + await this._walk(treeWalker); + // TreeWalker has reached a leaf node, reset the cursor to continue the traversal. + treeWalker.currentNode = firstChild; + } else { + this.characters(currentNode.textContent); + await this.endElement("", currentNode.localName, currentNode.nodeName); + return; + } + + // Traverse siblings next. + let nextSibling = treeWalker.nextSibling(); + while (nextSibling) { + await this._walk(treeWalker); + // TreeWalker has reached a leaf node, reset the cursor to continue the traversal. + treeWalker.currentNode = nextSibling; + nextSibling = treeWalker.nextSibling(); + } + + await this.endElement("", currentNode.localName, currentNode.nodeName); + } + } +} + +/** + * This is a handler for the etag request in calDavCalendar.js' getUpdatedItem. + * It uses XMLResponseHandler to parse the items and compose the resulting + * multiget. + */ +class CalDavEtagsHandler extends XMLResponseHandler { + /** + * @param {calDavCalendar} aCalendar - The (unwrapped) calendar this request belongs to. + * @param {nsIURI} aBaseUri - The URI requested (i.e inbox or collection). + * @param {*=} aChangeLogListener - (optional) for cached calendars, the listener to notify. + */ + constructor(aCalendar, aBaseUri, aChangeLogListener) { + super(); + this.calendar = aCalendar; + this.baseUri = aBaseUri; + this.changeLogListener = aChangeLogListener; + + this.itemsReported = {}; + this.itemsNeedFetching = []; + } + + skipIndex = -1; + currentResponse = null; + tag = null; + calendar = null; + baseUri = null; + changeLogListener = null; + logXML = ""; + + itemsReported = null; + itemsNeedFetching = null; + + QueryInterface = ChromeUtils.generateQI(["nsIRequestObserver", "nsIStreamListener"]); + + /** + * @see nsIRequestObserver + */ + onStartRequest(request) { + let httpchannel = request.QueryInterface(Ci.nsIHttpChannel); + + let responseStatus; + try { + responseStatus = httpchannel.responseStatus; + } catch (ex) { + cal.WARN("CalDAV: No response status getting etags for calendar " + this.calendar.name); + } + + if (responseStatus == 207) { + // We only need to parse 207's, anything else is probably a + // server error (i.e 50x). + httpchannel.contentType = "application/xml"; + } else { + cal.LOG("CalDAV: Error fetching item etags"); + this.calendar.reportDavError(Ci.calIErrors.DAV_REPORT_ERROR); + if (this.calendar.isCached && this.changeLogListener) { + this.changeLogListener.onResult({ status: Cr.NS_ERROR_FAILURE }, Cr.NS_ERROR_FAILURE); + } + } + } + + async onStopRequest(request, statusCode) { + let httpchannel = request.QueryInterface(Ci.nsIHttpChannel); + + let responseStatus; + try { + responseStatus = httpchannel.responseStatus; + } catch (ex) { + cal.WARN("CalDAV: No response status getting etags for calendar " + this.calendar.name); + } + + this.logResponse(responseStatus); + + if (responseStatus != 207) { + // Not a successful response, do nothing. + if (this.calendar.isCached && this.changeLogListener) { + this.changeLogListener.onResult({ status: Cr.NS_ERROR_FAILURE }, Cr.NS_ERROR_FAILURE); + } + return; + } + + await this.handleResponse(); + + // Now that we are done, check which items need fetching. + this.calendar.superCalendar.startBatch(); + + let needsRefresh = false; + try { + for (let path in this.calendar.mHrefIndex) { + if (path in this.itemsReported || path.substr(0, this.baseUri.length) == this.baseUri) { + // If the item is also on the server, check the next. + continue; + } + // If an item has been deleted from the server, delete it here too. + // Since the target calendar's operations are synchronous, we can + // safely set variables from this function. + let foundItem = await this.calendar.mOfflineStorage.getItem(this.calendar.mHrefIndex[path]); + + if (foundItem) { + let wasInboxItem = this.calendar.mItemInfoCache[foundItem.id].isInboxItem; + if ( + (wasInboxItem && this.calendar.isInbox(this.baseUri.spec)) || + (wasInboxItem === false && !this.calendar.isInbox(this.baseUri.spec)) + ) { + cal.LOG("Deleting local href: " + path); + delete this.calendar.mHrefIndex[path]; + await this.calendar.mOfflineStorage.deleteItem(foundItem); + needsRefresh = true; + } + } + } + } finally { + this.calendar.superCalendar.endBatch(); + } + + // Avoid sending empty multiget requests update views if something has + // been deleted server-side. + if (this.itemsNeedFetching.length) { + let multiget = new CalDavMultigetSyncHandler( + this.itemsNeedFetching, + this.calendar, + this.baseUri, + null, + false, + null, + this.changeLogListener + ); + multiget.doMultiGet(); + } else { + if (this.calendar.isCached && this.changeLogListener) { + this.changeLogListener.onResult({ status: Cr.NS_OK }, Cr.NS_OK); + } + + if (needsRefresh) { + this.calendar.mObservers.notify("onLoad", [this.calendar]); + } + + // but do poll the inbox + if (this.calendar.mShouldPollInbox && !this.calendar.isInbox(this.baseUri.spec)) { + this.calendar.pollInbox(); + } + } + } + + /** + * @see XMLResponseHandler + */ + fatalError() { + cal.WARN("CalDAV: Fatal Error parsing etags for " + this.calendar.name); + } + + /** + * @see XMLResponseHandler + */ + characters(aValue) { + if (this.calendar.verboseLogging()) { + this.logXML += aValue; + } + if (this.tag) { + this.currentResponse[this.tag] += aValue; + } + } + + startDocument() { + this.hrefMap = {}; + this.currentResponse = {}; + this.tag = null; + } + + endDocument() {} + + startElement(aUri, aLocalName, aQName, aAttributes) { + switch (aLocalName) { + case "response": + this.currentResponse = {}; + this.currentResponse.isCollection = false; + this.tag = null; + break; + case "collection": + this.currentResponse.isCollection = true; + // falls through + case "href": + case "getetag": + case "getcontenttype": + this.tag = aLocalName; + this.currentResponse[aLocalName] = ""; + break; + } + if (this.calendar.verboseLogging()) { + this.logXML += "<" + aQName + ">"; + } + } + + endElement(aUri, aLocalName, aQName) { + switch (aLocalName) { + case "response": { + this.tag = null; + let resp = this.currentResponse; + if ( + resp.getetag && + resp.getetag.length && + resp.href && + resp.href.length && + resp.getcontenttype && + resp.getcontenttype.length && + !resp.isCollection + ) { + resp.href = this.calendar.ensureDecodedPath(resp.href); + + if (resp.getcontenttype.substr(0, 14) == "message/rfc822") { + // workaround for a Scalix bug which causes incorrect + // contenttype to be returned. + resp.getcontenttype = "text/calendar"; + } + if (resp.getcontenttype == "text/vtodo") { + // workaround Kerio weirdness + resp.getcontenttype = "text/calendar"; + } + + // Only handle calendar items + if (resp.getcontenttype.substr(0, 13) == "text/calendar") { + if (resp.href && resp.href.length) { + this.itemsReported[resp.href] = resp.getetag; + + let itemUid = this.calendar.mHrefIndex[resp.href]; + if (!itemUid || resp.getetag != this.calendar.mItemInfoCache[itemUid].etag) { + this.itemsNeedFetching.push(resp.href); + } + } + } + } + break; + } + case "href": + case "getetag": + case "getcontenttype": { + this.tag = null; + break; + } + } + if (this.calendar.verboseLogging()) { + this.logXML += "</" + aQName + ">"; + } + } + + processingInstruction(aTarget, aData) {} +} + +/** + * This is a handler for the webdav sync request in calDavCalendar.js' + * getUpdatedItem. It uses XMLResponseHandler to parse the items and compose the + * resulting multiget. + */ +class CalDavWebDavSyncHandler extends XMLResponseHandler { + /** + * @param {calDavCalendar} aCalendar - The (unwrapped) calendar this request belongs to. + * @param {nsIURI} aBaseUri - The URI requested (i.e inbox or collection). + * @param {*=} aChangeLogListener - (optional) for cached calendars, the listener to notify. + */ + constructor(aCalendar, aBaseUri, aChangeLogListener) { + super(); + this.calendar = aCalendar; + this.baseUri = aBaseUri; + this.changeLogListener = aChangeLogListener; + + this.itemsReported = {}; + this.itemsNeedFetching = []; + } + + currentResponse = null; + tag = null; + calendar = null; + baseUri = null; + newSyncToken = null; + changeLogListener = null; + logXML = ""; + isInPropStat = false; + changeCount = 0; + unhandledErrors = 0; + itemsReported = null; + itemsNeedFetching = null; + additionalSyncNeeded = false; + + QueryInterface = ChromeUtils.generateQI(["nsIRequestObserver", "nsIStreamListener"]); + + async doWebDAVSync() { + if (this.calendar.mDisabledByDavError) { + // check if maybe our calendar has become available + this.calendar.checkDavResourceType(this.changeLogListener); + return; + } + + let syncTokenString = "<sync-token/>"; + if (this.calendar.mWebdavSyncToken && this.calendar.mWebdavSyncToken.length > 0) { + let syncToken = cal.xml.escapeString(this.calendar.mWebdavSyncToken); + syncTokenString = "<sync-token>" + syncToken + "</sync-token>"; + } + + let queryXml = + XML_HEADER + + '<sync-collection xmlns="DAV:">' + + syncTokenString + + "<sync-level>1</sync-level>" + + "<prop>" + + "<getcontenttype/>" + + "<getetag/>" + + "</prop>" + + "</sync-collection>"; + + let requestUri = this.calendar.makeUri(null, this.baseUri); + + if (this.calendar.verboseLogging()) { + cal.LOG(`CalDAV: send (REPORT ${requestUri.spec}): ${queryXml}`); + } + cal.LOG("CalDAV: webdav-sync Token: " + this.calendar.mWebdavSyncToken); + + let onSetupChannel = channel => { + // The depth header adheres to an older version of the webdav-sync + // spec and has been replaced by the <sync-level> tag above. + // Unfortunately some servers still depend on the depth header, + // therefore we send both (yuck). + channel.setRequestHeader("Depth", "1", false); + channel.requestMethod = "REPORT"; + }; + let request = new CalDavLegacySAXRequest( + this.calendar.session, + this.calendar, + requestUri, + queryXml, + MIME_TEXT_XML, + this, + onSetupChannel + ); + + await request.commit().catch(() => { + // Something went wrong with the OAuth token, notify failure + if (this.calendar.isCached && this.changeLogListener) { + this.changeLogListener.onResult( + { status: Cr.NS_ERROR_NOT_AVAILABLE }, + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + }); + } + + /** + * @see nsIRequestObserver + */ + onStartRequest(request) { + let httpchannel = request.QueryInterface(Ci.nsIHttpChannel); + + let responseStatus; + try { + responseStatus = httpchannel.responseStatus; + } catch (ex) { + cal.WARN("CalDAV: No response status doing webdav sync for calendar " + this.calendar.name); + } + + if (responseStatus == 207) { + // We only need to parse 207's, anything else is probably a + // server error (i.e 50x). + httpchannel.contentType = "application/xml"; + } + } + + async onStopRequest(request, statusCode) { + let httpchannel = request.QueryInterface(Ci.nsIHttpChannel); + + let responseStatus; + try { + responseStatus = httpchannel.responseStatus; + } catch (ex) { + cal.WARN("CalDAV: No response status doing webdav sync for calendar " + this.calendar.name); + } + + this.logResponse(responseStatus); + + if (responseStatus == 207) { + await this.handleResponse(); + } else if ( + (responseStatus == 403 && this._xmlString.includes(`<D:error xmlns:D="DAV:"/>`)) || + responseStatus == 429 + ) { + // We're hitting the rate limit. Don't attempt to refresh now. + cal.WARN("CalDAV: rate limit reached, server returned status code: " + responseStatus); + if (this.calendar.isCached && this.changeLogListener) { + // Not really okay, but we have to return something and an error code puts us in a bad state. + this.changeLogListener.onResult({ status: Cr.NS_ERROR_FAILURE }, Cr.NS_ERROR_FAILURE); + } + } else if ( + this.calendar.mWebdavSyncToken != null && + responseStatus >= 400 && + responseStatus <= 499 + ) { + // Invalidate sync token with 4xx errors that could indicate the + // sync token has become invalid and do a refresh. + cal.LOG( + "CalDAV: Resetting sync token because server returned status code: " + responseStatus + ); + this.calendar.mWebdavSyncToken = null; + this.calendar.saveCalendarProperties(); + this.calendar.safeRefresh(this.changeLogListener); + } else { + cal.WARN("CalDAV: Error doing webdav sync: " + responseStatus); + this.calendar.reportDavError(Ci.calIErrors.DAV_REPORT_ERROR); + if (this.calendar.isCached && this.changeLogListener) { + this.changeLogListener.onResult({ status: Cr.NS_ERROR_FAILURE }, Cr.NS_ERROR_FAILURE); + } + } + } + + /** + * @see XMLResponseHandler + */ + fatalError() { + cal.WARN("CalDAV: Fatal Error doing webdav sync for " + this.calendar.name); + } + + /** + * @see XMLResponseHandler + */ + characters(aValue) { + if (this.calendar.verboseLogging()) { + this.logXML += aValue; + } + this.currentResponse[this.tag] += aValue; + } + + startDocument() { + this.hrefMap = {}; + this.currentResponse = {}; + this.tag = null; + this.calendar.superCalendar.startBatch(); + } + + async endDocument() { + if (this.unhandledErrors) { + this.calendar.superCalendar.endBatch(); + this.calendar.reportDavError(Ci.calIErrors.DAV_REPORT_ERROR); + if (this.calendar.isCached && this.changeLogListener) { + this.changeLogListener.onResult({ status: Cr.NS_ERROR_FAILURE }, Cr.NS_ERROR_FAILURE); + } + return; + } + + if (this.calendar.mWebdavSyncToken == null && !this.additionalSyncNeeded) { + // null token means reset or first refresh indicating we did + // a full sync; remove local items that were not returned in this full + // sync + for (let path in this.calendar.mHrefIndex) { + if (!this.itemsReported[path]) { + await this.calendar.deleteTargetCalendarItem(path); + } + } + } + this.calendar.superCalendar.endBatch(); + + if (this.itemsNeedFetching.length) { + let multiget = new CalDavMultigetSyncHandler( + this.itemsNeedFetching, + this.calendar, + this.baseUri, + this.newSyncToken, + this.additionalSyncNeeded, + null, + this.changeLogListener + ); + multiget.doMultiGet(); + } else { + if (this.newSyncToken) { + this.calendar.mWebdavSyncToken = this.newSyncToken; + this.calendar.saveCalendarProperties(); + cal.LOG("CalDAV: New webdav-sync Token: " + this.calendar.mWebdavSyncToken); + + if (this.additionalSyncNeeded) { + let wds = new CalDavWebDavSyncHandler( + this.calendar, + this.baseUri, + this.changeLogListener + ); + wds.doWebDAVSync(); + return; + } + } + this.calendar.finalizeUpdatedItems(this.changeLogListener, this.baseUri); + } + } + + startElement(aUri, aLocalName, aQName, aAttributes) { + switch (aLocalName) { + case "response": // WebDAV Sync draft 3 + this.currentResponse = {}; + this.tag = null; + this.isInPropStat = false; + break; + case "propstat": + this.isInPropStat = true; + break; + case "status": + if (this.isInPropStat) { + this.tag = "propstat_" + aLocalName; + } else { + this.tag = aLocalName; + } + this.currentResponse[this.tag] = ""; + break; + case "href": + case "getetag": + case "getcontenttype": + case "sync-token": + this.tag = aLocalName.replace(/-/g, ""); + this.currentResponse[this.tag] = ""; + break; + } + if (this.calendar.verboseLogging()) { + this.logXML += "<" + aQName + ">"; + } + } + + async endElement(aUri, aLocalName, aQName) { + switch (aLocalName) { + case "response": // WebDAV Sync draft 3 + case "sync-response": { + // WebDAV Sync draft 0,1,2 + let resp = this.currentResponse; + if (resp.href && resp.href.length) { + resp.href = this.calendar.ensureDecodedPath(resp.href); + } + + if ( + (!resp.getcontenttype || resp.getcontenttype == "text/plain") && + resp.href && + resp.href.endsWith(".ics") + ) { + // If there is no content-type (iCloud) or text/plain was passed + // (iCal Server) for the resource but its name ends with ".ics" + // assume the content type to be text/calendar. Apple + // iCloud/iCal Server interoperability fix. + resp.getcontenttype = "text/calendar"; + } + + // Deleted item + if ( + resp.href && + resp.href.length && + resp.status && + resp.status.length && + resp.status.indexOf(" 404") > 0 + ) { + if (this.calendar.mHrefIndex[resp.href]) { + this.changeCount++; + await this.calendar.deleteTargetCalendarItem(resp.href); + } else { + cal.LOG("CalDAV: skipping unfound deleted item : " + resp.href); + } + // Only handle Created or Updated calendar items + } else if ( + resp.getcontenttype && + resp.getcontenttype.substr(0, 13) == "text/calendar" && + resp.getetag && + resp.getetag.length && + resp.href && + resp.href.length && + (!resp.status || // Draft 3 does not require + resp.status.length == 0 || // a status for created or updated items but + resp.status.indexOf(" 204") || // draft 0, 1 and 2 needed it so treat no status + resp.status.indexOf(" 200") || // Apple iCloud returns 200 status for each item + resp.status.indexOf(" 201")) + ) { + // and status 201 and 204 the same + this.itemsReported[resp.href] = resp.getetag; + let itemId = this.calendar.mHrefIndex[resp.href]; + let oldEtag = itemId && this.calendar.mItemInfoCache[itemId].etag; + + if (!oldEtag || oldEtag != resp.getetag) { + // Etag mismatch, getting new/updated item. + this.itemsNeedFetching.push(resp.href); + } + } else if (resp.status && resp.status.includes(" 507")) { + // webdav-sync says that if a 507 is encountered and the + // url matches the request, the current token should be + // saved and another request should be made. We don't + // actually compare the URL, its too easy to get this + // wrong. + + // The 507 doesn't mean the data received is invalid, so + // continue processing. + this.additionalSyncNeeded = true; + } else if ( + resp.status && + resp.status.indexOf(" 200") && + resp.href && + resp.href.endsWith("/") + ) { + // iCloud returns status responses for directories too + // so we just ignore them if they have status code 200. We + // want to make sure these are not counted as unhandled + // errors in the next block + } else if ( + (resp.getcontenttype && resp.getcontenttype.startsWith("text/calendar")) || + (resp.status && !resp.status.includes(" 404")) + ) { + // If the response element is still not handled, log an + // error only if the content-type is text/calendar or the + // response status is different than 404 not found. We + // don't care about response elements on non-calendar + // resources or whose status is not indicating a deleted + // resource. + cal.WARN("CalDAV: Unexpected response, status: " + resp.status + ", href: " + resp.href); + this.unhandledErrors++; + } else { + cal.LOG( + "CalDAV: Unhandled response element, status: " + + resp.status + + ", href: " + + resp.href + + " contenttype:" + + resp.getcontenttype + ); + } + break; + } + case "sync-token": { + this.newSyncToken = this.currentResponse[this.tag]; + break; + } + case "propstat": { + this.isInPropStat = false; + break; + } + } + this.tag = null; + if (this.calendar.verboseLogging()) { + this.logXML += "</" + aQName + ">"; + } + } + + processingInstruction(aTarget, aData) {} +} + +/** + * This is a handler for the multiget request. It uses XMLResponseHandler to + * parse the items and compose the resulting multiget. + */ +class CalDavMultigetSyncHandler extends XMLResponseHandler { + /** + * @param {string[]} aItemsNeedFetching - Array of items to fetch, an array of + * un-encoded paths. + * @param {calDavCalendar} aCalendar - The (unwrapped) calendar this request belongs to. + * @param {nsIURI} aBaseUri - The URI requested (i.e inbox or collection). + * @param {*=} aNewSyncToken - (optional) New Sync token to set if operation successful. + * @param {boolean=} aAdditionalSyncNeeded - (optional) If true, the passed sync token is not the + * latest, another webdav sync run should be + * done after completion. + * @param {*=} aListener - (optional) The listener to notify. + * @param {*=} aChangeLogListener - (optional) For cached calendars, the listener to + * notify. + */ + constructor( + aItemsNeedFetching, + aCalendar, + aBaseUri, + aNewSyncToken, + aAdditionalSyncNeeded, + aListener, + aChangeLogListener + ) { + super(); + this.calendar = aCalendar; + this.baseUri = aBaseUri; + this.listener = aListener; + this.newSyncToken = aNewSyncToken; + this.changeLogListener = aChangeLogListener; + this.itemsNeedFetching = aItemsNeedFetching; + this.additionalSyncNeeded = aAdditionalSyncNeeded; + } + + currentResponse = null; + tag = null; + calendar = null; + baseUri = null; + newSyncToken = null; + listener = null; + changeLogListener = null; + logXML = null; + unhandledErrors = 0; + itemsNeedFetching = null; + additionalSyncNeeded = false; + timer = null; + + QueryInterface = ChromeUtils.generateQI(["nsIRequestObserver", "nsIStreamListener"]); + + doMultiGet() { + if (this.calendar.mDisabledByDavError) { + // check if maybe our calendar has become available + this.calendar.checkDavResourceType(this.changeLogListener); + return; + } + + let batchSize = Services.prefs.getIntPref("calendar.caldav.multigetBatchSize", 100); + let hrefString = ""; + while (this.itemsNeedFetching.length && batchSize > 0) { + batchSize--; + // ensureEncodedPath extracts only the path component of the item and + // encodes it before it is sent to the server + let locpath = this.calendar.ensureEncodedPath(this.itemsNeedFetching.pop()); + hrefString += "<D:href>" + cal.xml.escapeString(locpath) + "</D:href>"; + } + + let queryXml = + XML_HEADER + + '<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">' + + "<D:prop>" + + "<D:getetag/>" + + "<C:calendar-data/>" + + "</D:prop>" + + hrefString + + "</C:calendar-multiget>"; + + let requestUri = this.calendar.makeUri(null, this.baseUri); + if (this.calendar.verboseLogging()) { + cal.LOG(`CalDAV: send (REPORT ${requestUri.spec}): ${queryXml}`); + } + + let onSetupChannel = channel => { + channel.requestMethod = "REPORT"; + channel.setRequestHeader("Depth", "1", false); + }; + let request = new CalDavLegacySAXRequest( + this.calendar.session, + this.calendar, + requestUri, + queryXml, + MIME_TEXT_XML, + this, + onSetupChannel + ); + + request.commit().catch(() => { + // Something went wrong with the OAuth token, notify failure + if (this.calendar.isCached && this.changeLogListener) { + this.changeLogListener.onResult( + { status: Cr.NS_ERROR_NOT_AVAILABLE }, + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + }); + } + + /** + * @see nsIRequestObserver + */ + onStartRequest(request) { + let httpchannel = request.QueryInterface(Ci.nsIHttpChannel); + + let responseStatus; + try { + responseStatus = httpchannel.responseStatus; + } catch (ex) { + cal.WARN("CalDAV: No response status doing multiget for calendar " + this.calendar.name); + } + + if (responseStatus == 207) { + // We only need to parse 207's, anything else is probably a + // server error (i.e 50x). + httpchannel.contentType = "application/xml"; + } else { + let errorMsg = + "CalDAV: Error: got status " + + responseStatus + + " fetching calendar data for " + + this.calendar.name + + ", " + + this.listener; + this.calendar.notifyGetFailed(errorMsg, this.listener, this.changeLogListener); + } + } + + async onStopRequest(request, statusCode) { + let httpchannel = request.QueryInterface(Ci.nsIHttpChannel); + + let responseStatus; + try { + responseStatus = httpchannel.responseStatus; + } catch (ex) { + cal.WARN("CalDAV: No response status doing multiget for calendar " + this.calendar.name); + } + + this.logResponse(responseStatus); + + if (responseStatus != 207) { + // Not a successful response, do nothing. + if (this.calendar.isCached && this.changeLogListener) { + this.changeLogListener.onResult({ status: Cr.NS_ERROR_FAILURE }, Cr.NS_ERROR_FAILURE); + } + return; + } + + if (this.unhandledErrors) { + this.calendar.superCalendar.endBatch(); + this.calendar.notifyGetFailed("multiget error", this.listener, this.changeLogListener); + return; + } + if (this.itemsNeedFetching.length == 0) { + if (this.newSyncToken) { + this.calendar.mWebdavSyncToken = this.newSyncToken; + this.calendar.saveCalendarProperties(); + cal.LOG("CalDAV: New webdav-sync Token: " + this.calendar.mWebdavSyncToken); + } + } + await this.handleResponse(); + if (this.itemsNeedFetching.length > 0) { + cal.LOG("CalDAV: Still need to fetch " + this.itemsNeedFetching.length + " elements."); + this.resetXMLResponseHandler(); + let timerCallback = { + requestHandler: this, + notify(timer) { + // Call multiget again to get another batch + this.requestHandler.doMultiGet(); + }, + }; + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.timer.initWithCallback(timerCallback, 0, Ci.nsITimer.TYPE_ONE_SHOT); + } else if (this.additionalSyncNeeded) { + let wds = new CalDavWebDavSyncHandler(this.calendar, this.baseUri, this.changeLogListener); + wds.doWebDAVSync(); + } else { + this.calendar.finalizeUpdatedItems(this.changeLogListener, this.baseUri); + } + } + + /** + * @see XMLResponseHandler + */ + fatalError(error) { + cal.WARN("CalDAV: Fatal Error doing multiget for " + this.calendar.name + ": " + error); + } + + /** + * @see XMLResponseHandler + */ + characters(aValue) { + if (this.calendar.verboseLogging()) { + this.logXML += aValue; + } + if (this.tag) { + this.currentResponse[this.tag] += aValue; + } + } + + startDocument() { + this.hrefMap = {}; + this.currentResponse = {}; + this.tag = null; + this.logXML = ""; + this.calendar.superCalendar.startBatch(); + } + + endDocument() { + this.calendar.superCalendar.endBatch(); + } + + startElement(aUri, aLocalName, aQName, aAttributes) { + switch (aLocalName) { + case "response": + this.currentResponse = {}; + this.tag = null; + this.isInPropStat = false; + break; + case "propstat": + this.isInPropStat = true; + break; + case "status": + if (this.isInPropStat) { + this.tag = "propstat_" + aLocalName; + } else { + this.tag = aLocalName; + } + this.currentResponse[this.tag] = ""; + break; + case "calendar-data": + case "href": + case "getetag": + this.tag = aLocalName.replace(/-/g, ""); + this.currentResponse[this.tag] = ""; + break; + } + if (this.calendar.verboseLogging()) { + this.logXML += "<" + aQName + ">"; + } + } + + async endElement(aUri, aLocalName, aQName) { + switch (aLocalName) { + case "response": { + let resp = this.currentResponse; + if (resp.href && resp.href.length) { + resp.href = this.calendar.ensureDecodedPath(resp.href); + } + if ( + resp.href && + resp.href.length && + resp.status && + resp.status.length && + resp.status.indexOf(" 404") > 0 + ) { + if (this.calendar.mHrefIndex[resp.href]) { + await this.calendar.deleteTargetCalendarItem(resp.href); + } else { + cal.LOG("CalDAV: skipping unfound deleted item : " + resp.href); + } + // Created or Updated item + } else if ( + resp.getetag && + resp.getetag.length && + resp.href && + resp.href.length && + resp.calendardata && + resp.calendardata.length + ) { + let oldEtag; + let itemId = this.calendar.mHrefIndex[resp.href]; + if (itemId) { + oldEtag = this.calendar.mItemInfoCache[itemId].etag; + } else { + oldEtag = null; + } + if (!oldEtag || oldEtag != resp.getetag || this.listener) { + await this.calendar.addTargetCalendarItem( + resp.href, + resp.calendardata, + this.baseUri, + resp.getetag, + this.listener + ); + } else { + cal.LOG("CalDAV: skipping item with unmodified etag : " + oldEtag); + } + } else { + cal.WARN( + "CalDAV: Unexpected response, status: " + + resp.status + + ", href: " + + resp.href + + " calendar-data:\n" + + resp.calendardata + ); + this.unhandledErrors++; + } + break; + } + case "propstat": { + this.isInPropStat = false; + break; + } + } + this.tag = null; + if (this.calendar.verboseLogging()) { + this.logXML += "</" + aQName + ">"; + } + } + + processingInstruction(aTarget, aData) {} +} |